package match // levenshtein returns the edit distance between two strings, measured in // runes. It uses the rolling-row formulation: O(min(n,m)) memory. // // The resolver uses it only to tolerate typos in the *street name*; house // numbers are always compared exactly, because a wrong house is worse than a // missing one. func levenshtein(a, b string) int { ra, rb := []rune(a), []rune(b) if len(ra) == 0 { return len(rb) } if len(rb) == 0 { return len(ra) } prev := make([]int, len(rb)+1) curr := make([]int, len(rb)+1) for j := range prev { prev[j] = j } for i := 1; i <= len(ra); i++ { curr[0] = i for j := 1; j <= len(rb); j++ { cost := 1 if ra[i-1] == rb[j-1] { cost = 0 } curr[j] = min3(prev[j]+1, curr[j-1]+1, prev[j-1]+cost) } prev, curr = curr, prev } return prev[len(rb)] } // ratio returns a 0..1 similarity where 1 means identical. It is // 1 - distance/max(len), which is stable and easy to reason about. func ratio(a, b string) float64 { if a == b { return 1 } la, lb := len([]rune(a)), len([]rune(b)) m := la if lb > m { m = lb } if m == 0 { return 1 } return 1 - float64(levenshtein(a, b))/float64(m) } func min3(a, b, c int) int { m := a if b < m { m = b } if c < m { m = c } return m }