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:
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user