Knowing a query matches is not enough - to score and highlight a match you need to know exactly which candidate characters it landed on. Today the matcher starts reporting those positions.
Change the matcher to return the list of candidate indices where each query character matched, using the greedy leftmost choice.
A boolean answer was enough to filter, but everything ahead - scoring a match, highlighting it, ranking one candidate above another - needs to know which characters the query matched. So the matcher graduates from returning true to returning a list of positions: one candidate index per query character, in order.
The rule for which index is the greedy leftmost one: for each query character, take the first candidate character that fits. That is the natural output of the two-cursor walk you already have - just record each index as you consume it. Greedy positions are not always the best-looking match (a later lesson finds a higher-scoring alignment), but they are correct, cheap, and the right foundation. Note the edges: no match returns no positions and a false flag, and an empty query matches with an empty position list - a real match that highlights nothing.
// Same two-cursor walk, but record each candidate index you consume.// "Greedy leftmost": take the first candidate char that fits each// query char. Return the positions plus whether it matched at all.func matchPositions(query, candidate string) ([]int, bool) {pos := []int{}qi := 0for ci := 0; qi < len(query) && ci < len(candidate); ci++ {if equalSmartCase(query[qi], candidate[ci], query) {pos = append(pos, ci)qi++}}return pos, qi == len(query)}