- add internal/eval: deterministic citizen-style perturbation harness (typos, case, word order, abbreviations, filler words, dropped units) - parser: strip all street-type tokens, filler words (город/дом/д/область), trailing apartment (кв N); handle letter-before-fraction and corpus letters - matching: token-aware typo-tolerant street similarity; wider ambiguity margin to surface near-collisions instead of guessing - seed: correct parse-rate accounting, structure-aware de-duplication - CLI: 'addrkit eval' command; README with measured results Measured on the real 73,936-row registry: 99.98% parsed, 0 wrong-house safety violations across 34k+ perturbed cases.
78 lines
3.7 KiB
Go
78 lines
3.7 KiB
Go
package address
|
||
|
||
import "strings"
|
||
|
||
// fillerWords are tokens that carry no street-name meaning and that people
|
||
// routinely include ("г. Новосибирск", "улица ... дом 5", "... область").
|
||
// Dropping them makes the service tolerant of natural phrasing. District
|
||
// adjectives are deliberately NOT dropped: they can legitimately be part of a
|
||
// street name.
|
||
var fillerWords = map[string]bool{
|
||
"новосибирск": true, "нск": true, "нсо": true,
|
||
"город": true, "г": true,
|
||
"область": true, "обл": true,
|
||
"район": true, "дом": true, "д": true,
|
||
}
|
||
|
||
// streetTypes maps a surface form (including common abbreviations) to a single
|
||
// canonical street type. The type is informational for matching (we strip it
|
||
// from the key) but useful for disambiguation and display.
|
||
var streetTypes = map[string]string{
|
||
"ул": "улица", "улица": "улица",
|
||
"пер": "переулок", "пер-к": "переулок", "переулок": "переулок",
|
||
"пр": "проезд", "проезд": "проезд",
|
||
"пр-т": "проспект", "просп": "проспект", "проспект": "проспект",
|
||
"ш": "шоссе", "шоссе": "шоссе",
|
||
"мкр": "микрорайон", "микрорайон": "микрорайон",
|
||
"туп": "тупик", "тупик": "тупик",
|
||
"спуск": "спуск",
|
||
"б-р": "бульвар", "бул": "бульвар", "бульвар": "бульвар",
|
||
"пл": "площадь", "площадь": "площадь",
|
||
"пос": "поселок", "поселок": "поселок",
|
||
"тракт": "тракт",
|
||
"аллея": "аллея",
|
||
"наб": "набережная", "набережная": "набережная",
|
||
"кольцо": "кольцо",
|
||
"просека": "просека",
|
||
"квартал": "квартал",
|
||
"территория": "территория",
|
||
"дорога": "дорога",
|
||
"разъезд": "разъезд",
|
||
}
|
||
|
||
// ordinalWords maps spelled-out ordinals to their digit form. Only the forms
|
||
// that actually occur in practice are listed; unknown words are treated as
|
||
// part of the street name.
|
||
var ordinalWords = map[string]string{"первый": "1", "первая": "1", "первое": "1",
|
||
"второй": "2", "вторая": "2", "второе": "2",
|
||
"третий": "3", "третья": "3", "третье": "3",
|
||
"четвертый": "4", "четвертая": "4", "четвертое": "4",
|
||
"пятый": "5", "пятая": "5", "пятое": "5",
|
||
"шестой": "6", "шестая": "6", "шестое": "6",
|
||
"седьмой": "7", "седьмая": "7", "седьмое": "7",
|
||
"восьмой": "8", "восьмая": "8", "восьмое": "8",
|
||
"девятый": "9", "девятая": "9", "девятое": "9",
|
||
"десятый": "10", "десятая": "10", "десятое": "10",
|
||
}
|
||
|
||
// ordinalSuffixes are the gender/number endings that may follow an ordinal
|
||
// digit when it is written as a separate token ("2 я", "10 й").
|
||
var ordinalSuffixes = map[string]bool{
|
||
"я": true, "й": true, "е": true,
|
||
"ая": true, "ый": true, "ой": true, "ое": true,
|
||
"ий": true, "ья": true, "ье": true, "ьи": true,
|
||
}
|
||
|
||
// streetKey builds the canonical, type-independent street key:
|
||
// "<ordinal>|<sorted name tokens>". The type word is dropped so that a user
|
||
// who omits it ("станционная 1" instead of "Станционная улица 1") still
|
||
// matches; type confusion is surfaced later as ambiguity.
|
||
func streetKey(ordinal, nameKey string) string {
|
||
return ordinal + "|" + nameKey
|
||
}
|
||
|
||
// normalizeOrdinal trims leading zeros so "02-я" and "2-я" agree.
|
||
func normalizeOrdinal(s string) string {
|
||
return trimLeadingZeros(strings.TrimSpace(s))
|
||
}
|