package match import ( "testing" "git.totmin.ru/en2zmax/addrkit/internal/domain/address" ) // buildIndex parses canonical addresses into entries, mirroring what the seed // pipeline does, so tests exercise the real code path end to end. func buildIndex(t *testing.T, addresses ...string) *Index { t.Helper() entries := make([]address.Entry, 0, len(addresses)) for i, a := range addresses { p := address.Parse(a) entries = append(entries, address.Entry{ ID: a, Address: a, District: "Тестовый", Ordinal: p.Ordinal, NameKey: p.NameKey, StreetType: p.StreetType, StreetKey: p.StreetKey, House: p.House, HouseFrac: p.HouseFrac, HouseLetter: p.HouseLetter, Building: p.Building, BuildingFrac: p.BuildingFrac, BuildingLetter: p.BuildingLetter, Structure: p.Structure, }) _ = i } return NewIndex("test", entries, DefaultOptions()) } func TestResolveStatuses(t *testing.T) { ix := buildIndex(t, "Станционная улица 1/1", "2-я Станционная улица 1/1", "Станционная улица 2/1", "Станционная улица 3/1 к1", "Станционная улица 3/1 к2", "Искитимская улица 5", ) tests := []struct { name string query string wantStatus address.Status wantAddr string }{ { name: "found with ordinal, fraction and structure noise", query: "2-я Станционная улица 1/1 стр.", wantStatus: address.StatusFound, wantAddr: "2-я Станционная улица 1/1", }, { name: "found exact house", query: "станционная 2/1", wantStatus: address.StatusFound, wantAddr: "Станционная улица 2/1", }, { name: "found with corpus", query: "Станционная улица 3/1 корпус 1", wantStatus: address.StatusFound, wantAddr: "Станционная улица 3/1 к1", }, { name: "typo in street name tolerated", query: "искитимсая 5", wantStatus: address.StatusFound, wantAddr: "Искитимская улица 5", }, { name: "missing house is partial", query: "станционная улица", wantStatus: address.StatusPartial, }, { name: "wrong house fraction is partial", query: "станционная 1/2", wantStatus: address.StatusPartial, }, { name: "ambiguous building", query: "станционная 3/1", wantStatus: address.StatusAmbiguous, }, { name: "unknown street", query: "ленина 5", wantStatus: address.StatusNotFound, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { res := ix.Resolve(address.Parse(tt.query)) if res.Status != tt.wantStatus { t.Fatalf("status = %q, want %q (candidates: %+v)", res.Status, tt.wantStatus, res.Candidates) } if tt.wantAddr != "" && res.Address != tt.wantAddr { t.Errorf("address = %q, want %q", res.Address, tt.wantAddr) } if res.DataVersion != "test" { t.Errorf("data version = %q, want test", res.DataVersion) } }) } } // TestResolveNeverInventsAddress is the central safety property: whenever the // status is "found", the returned address must exist in the index. func TestResolveNeverInventsAddress(t *testing.T) { addresses := []string{ "Станционная улица 1/1", "Станционная улица 2/1", "Искитимская улица 5", } ix := buildIndex(t, addresses...) known := make(map[string]bool, len(addresses)) for _, a := range addresses { known[a] = true } queries := []string{ "2-я Станционная 1/1", "станционная 2/1 к9", "искитимская 5", "искитимская 5/1", "несуществующая 99", "станционная 1/1 стр.3", " ", } for _, q := range queries { res := ix.Resolve(address.Parse(q)) if res.Status == address.StatusFound && !known[res.Address] { t.Fatalf("invented address %q for query %q", res.Address, q) } } } func TestRatio(t *testing.T) { if got := ratio("станционная", "станционная"); got != 1 { t.Errorf("identical strings ratio = %v, want 1", got) } if got := ratio("станционная", "искитимская"); got > 0.6 { t.Errorf("unrelated strings ratio = %v, want < 0.6", got) } }