feat(pkg): expose a public library facade

Another repository can import pkg/addrkit and use the parser, index and resolver without HTTP or SQLite. The package only re-exports domain types and constructors and performs no I/O.
This commit is contained in:
Maksim Totmin
2026-10-27 14:35:57 +07:00
parent 17ff5c591f
commit e5e0ffedc0
2 changed files with 161 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
// Package addrkit is the public library facade of the address normalizer.
//
// The service (cmd/addrkitd) and the CLI use the same domain packages through
// this API. Another project in a different repository can import this package
// directly instead of calling the HTTP service; nothing here does I/O, opens a
// database or depends on the service configuration.
//
// Example:
//
// parser := addrkit.NewParser(
// addrkit.NewDictionary([]addrkit.City{
// {Name: "Новосибирск", Aliases: []string{"нск"}},
// }),
// "",
// )
// parsed := parser.Parse("нск, Ленина 5")
//
// index := addrkit.NewIndex("2026-10-01", entries, addrkit.DefaultOptions())
// resolver := addrkit.NewResolver(index, nil)
// result := resolver.Resolve(context.Background(), "нск, Ленина 5")
package addrkit
import (
"git.totmin.ru/en2zmax/addrkit/internal/domain/address"
"git.totmin.ru/en2zmax/addrkit/internal/domain/match"
)
// Core value types.
type (
// City is one entry of the configurable city dictionary.
City = address.City
// ParsedAddress is the structured form of a free-form address.
ParsedAddress = address.ParsedAddress
// Entry is one canonical registry row.
Entry = address.Entry
// Status is the machine-readable outcome of a resolution.
Status = address.Status
)
// Resolution statuses.
const (
StatusFound = address.StatusFound
StatusPartial = address.StatusPartial
StatusAmbiguous = address.StatusAmbiguous
StatusNotFound = address.StatusNotFound
)
// Dictionary is the immutable vocabulary used by the parser.
type Dictionary = address.Dictionary
// NewDictionary builds a dictionary with the built-in vocabulary and cities.
func NewDictionary(cities []City) *Dictionary {
return address.NewDictionary(cities)
}
// Parser turns free-form address strings into structured addresses.
type Parser = address.Parser
// NewParser builds a parser. defaultCity may be empty.
func NewParser(dict *Dictionary, defaultCity string) *Parser {
return address.NewParser(dict, defaultCity)
}
// Parse uses the shared city-less parser.
func Parse(raw string) ParsedAddress {
return address.Parse(raw)
}
// Matching and resolution.
type (
// Options tunes the index.
Options = match.Options
// Candidate is one plausible match.
Candidate = match.Candidate
// Result is the outcome of a resolution.
Result = match.Result
// Rewriter is the optional query rewriter (for example an LLM).
Rewriter = match.Rewriter
// Index is an immutable in-memory registry index.
Index = match.Index
// Resolver resolves parsed addresses against an index.
Resolver = match.Resolver
)
// ResolverOptions tunes optional resolver behaviour.
type ResolverOptions struct {
// Parser defaults to a city-less parser.
Parser *Parser
// OnRewriteError observes failures of the optional rewriter. The resolver
// always keeps the deterministic result.
OnRewriteError func(error)
}
// DefaultOptions returns the conservative matcher defaults.
func DefaultOptions() Options {
return match.DefaultOptions()
}
// NewIndex builds an index from canonical entries.
func NewIndex(version string, entries []Entry, opts Options) *Index {
return match.NewIndex(version, entries, opts)
}
// NewResolver builds a resolver with the default parser.
func NewResolver(index *Index, rewriter Rewriter) *Resolver {
return match.NewResolver(index, rewriter)
}
// NewResolverWithOptions builds a resolver with explicit options.
func NewResolverWithOptions(index *Index, rewriter Rewriter, opts ResolverOptions) *Resolver {
return match.NewResolverWithOptions(index, rewriter, match.ResolverOptions{
Parser: opts.Parser,
OnRewriteError: opts.OnRewriteError,
})
}
+46
View File
@@ -0,0 +1,46 @@
package addrkit_test
import (
"context"
"testing"
"git.totmin.ru/en2zmax/addrkit/pkg/addrkit"
)
// TestPublicAPI exercises the facade exactly the way another repository would:
// parse a city-aware query and resolve it against an in-memory index.
func TestPublicAPI(t *testing.T) {
dict := addrkit.NewDictionary([]addrkit.City{
{Name: "Новосибирск", Aliases: []string{"нск"}},
{Name: "Бердск"},
})
parser := addrkit.NewParser(dict, "")
entries := make([]addrkit.Entry, 0, 2)
for _, city := range []string{"Новосибирск", "Бердск"} {
p := parser.ParseInCity("Ленина улица 5", city)
entries = append(entries, addrkit.Entry{
ID: city, Address: "Ленина улица 5",
City: p.City, CityKey: p.CityKey,
Ordinal: p.Ordinal, NameKey: p.NameKey, NameDisplay: p.NameDisplay,
StreetType: p.StreetType, StreetKey: p.StreetKey,
House: p.House,
})
}
resolver := addrkit.NewResolverWithOptions(
addrkit.NewIndex("2026-10-01", entries, addrkit.DefaultOptions()),
nil,
addrkit.ResolverOptions{Parser: parser},
)
res := resolver.ResolveInCity(context.Background(), "нск, Ленина 5", "")
if res.Status != addrkit.StatusFound || res.City != "Новосибирск" {
t.Fatalf("result = %+v", res)
}
ambiguous := resolver.Resolve(context.Background(), "Ленина 5")
if ambiguous.Status != addrkit.StatusAmbiguous {
t.Fatalf("cityless result = %+v", ambiguous)
}
}