Filtering tells you what matches; ranking needs to know how well. Today you give a match a numeric score - a reward per matched character and a small penalty for every character skipped between matches.
Score a match given its positions, awarding a fixed amount per matched character and a penalty per skipped character between matches.
A finder that only filters leaves the best matches buried among mediocre ones. Scoring fixes that: it turns a match into a number so the good matches can float to the top. The base model is deliberately simple - every matched character is worth a fixed +16, and every candidate character you have to skip between two matched characters costs -1. A tight match (few gaps) scores higher than a scattered one.
The only subtlety is what a “gap” is: the number of candidate characters strictly between two consecutive matched positions, which is thisPos - prevPos - 1. Positions [0, 2] skip one character (index 1); [0, 3] skip two. This per-character gap penalty is the seed of the whole model - the next lessons layer bonuses and one more penalty on top, but they all plug into this same left-to-right walk over the positions.
// Sum a fixed reward per matched char; subtract the size of each gap// BETWEEN matched chars (skipped = thisPos - prevPos - 1).const scoreMatch = 16func score(candidate string, pos []int) int {s, prev := 0, -1for k, p := range pos {s += scoreMatchif k > 0 {gap := p - prev - 1s -= gap}prev = p}return s}