Real queries have spaces - "src go" should mean "matches src AND matches go". Today you split the query into terms and require every one to match, summing their scores.
Split the query on spaces into terms and require a candidate to match all of them, scoring the sum and combining the positions.
Once queries get real, they get spaces. Typing src go for src/main.go means “the candidate must contain a src-ish run and a go-ish run” - two independent fuzzy matches, both required. So the query stops being one string and becomes a list of terms, split on whitespace, and a candidate is a match only if every term matches it.
Scoring composes naturally: run each term’s best-alignment score and sum them, and take the union of their positions for highlighting. This is also the moment to introduce the Term type with room for the flags the next lessons need - exact, anchored, negated - even though today every term is a plain fuzzy term. Giving Term its full shape now means those lessons only set a flag and branch, instead of forcing a rewrite of this matching loop. The single new idea today is “all terms must fit”; the flags stay dormant until their lessons.
// A query is a list of terms. Front-load the shape now; later lessons// fill in the flags.type Term struct {Text stringExact boolAnchorStart boolAnchorEnd boolNegate bool}func parseQuery(q string) []Term { /* split on spaces -> Term{Text: w} */ }// Candidate matches iff EVERY term matches; score = sum, positions =// sorted union across terms.