If a player types “refrigerator” and the answer is “fridge”, or types “narut” for Naruto, the game has to accept it. If it accepts a wrong answer, the game feels rigged. Matching sits on the critical path of every claim, so it runs on the server with a 200 ms end-to-end budget.
The matcher is a chain of layers behind one interface. Normalized exact matches and generated synonyms resolve through hash lookups. Only when both miss does a typo-tolerant fallback run, against the board’s answers already held in memory.
The first fallback was a semantic embedding model. I measured it on a fandom board and found it accepted unrelated short names as matches, with no threshold that separated real typos from noise. I replaced it with a guarded edit-distance layer: a minimum input length, an edit budget that scales with length, a first-character anchor and rejection of ties. It is stricter, needs no model in memory and is faster.
server/src/matching/fuzzy.ts (excerpt)// Short inputs get one edit; longer ones, where a typo is
// a smaller share of the word, get two or three.
function editBudget(length) {
if (length <= 7) return 1
if (length <= 11) return 2
return 3
}
const consider = (candidate, answerId) => {
// A real near-miss keeps its first letter
if (candidate[0] !== normalized[0]) return
const distance = editDistance(normalized, candidate, bestDistance)
if (distance > budget) return
if (distance < bestDistance) { bestDistance = distance; bestAnswerId = answerId }
else if (distance === bestDistance && answerId !== bestAnswerId) ambiguous = true
}