addrkit: deterministic address resolution service (Go)
- domain: address normalization/parsing, matching with explicit statuses - adapters: SQLite registry store, ArcGIS outages client, optional LLM rewriter, HTTP API - seed pipeline with duplicate handling and unparsed reporting - CLI (seed/verify/resolve), systemd unit, OpenAPI contract, tests
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package match
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.totmin.ru/en2zmax/addrkit/internal/domain/address"
|
||||
)
|
||||
|
||||
// Rewriter is an optional, pluggable query rewriter. Its only job is to
|
||||
// propose alternative surface forms of a user address (for example an LLM
|
||||
// normalising "кошурникова одиннадцать дробь один" to "Кошевого 11/1"). The
|
||||
// rewriter never decides the answer: every proposal is parsed and resolved
|
||||
// deterministically, and only entries that exist in the index can be returned.
|
||||
//
|
||||
// It is defined here (consumer side) so the domain stays free of any transport
|
||||
// or vendor dependency.
|
||||
type Rewriter interface {
|
||||
Rewrite(ctx context.Context, raw string) ([]string, error)
|
||||
}
|
||||
|
||||
// Resolver wraps an Index with an optional Rewriter.
|
||||
//
|
||||
// The deterministic path is always tried first. The rewriter is consulted only
|
||||
// when the deterministic result is not a confident "found", and its
|
||||
// suggestions can only ever turn a miss into a match against real data.
|
||||
type Resolver struct {
|
||||
index *Index
|
||||
rewriter Rewriter
|
||||
}
|
||||
|
||||
// NewResolver builds a resolver. rewriter may be nil to disable rewriting.
|
||||
func NewResolver(index *Index, rewriter Rewriter) *Resolver {
|
||||
return &Resolver{index: index, rewriter: rewriter}
|
||||
}
|
||||
|
||||
// Version returns the current data version.
|
||||
func (r *Resolver) Version() string { return r.index.Version() }
|
||||
|
||||
// Ready reports whether the resolver has a usable index.
|
||||
func (r *Resolver) Ready() bool { return r.index != nil && r.index.Size() > 0 }
|
||||
|
||||
// Resolve parses raw and resolves it, consulting the rewriter when enabled.
|
||||
func (r *Resolver) Resolve(ctx context.Context, raw string) Result {
|
||||
parsed := address.Parse(raw)
|
||||
res := r.index.Resolve(parsed)
|
||||
|
||||
if res.Status == address.StatusFound || r.rewriter == nil {
|
||||
return res
|
||||
}
|
||||
if res.Status != address.StatusNotFound && res.Status != address.StatusAmbiguous {
|
||||
return res
|
||||
}
|
||||
|
||||
alternatives, err := r.rewriter.Rewrite(ctx, raw)
|
||||
if err != nil || len(alternatives) == 0 {
|
||||
return res
|
||||
}
|
||||
|
||||
best := res
|
||||
for _, alt := range alternatives {
|
||||
candidate := r.index.Resolve(address.Parse(alt))
|
||||
if better(candidate, best) {
|
||||
best = candidate
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// better ranks results: a stronger status wins, then higher confidence.
|
||||
func better(a, b Result) bool {
|
||||
ra, rb := statusRank(a.Status), statusRank(b.Status)
|
||||
if ra != rb {
|
||||
return ra > rb
|
||||
}
|
||||
return a.Confidence > b.Confidence
|
||||
}
|
||||
|
||||
func statusRank(s address.Status) int {
|
||||
switch s {
|
||||
case address.StatusFound:
|
||||
return 3
|
||||
case address.StatusAmbiguous:
|
||||
return 2
|
||||
case address.StatusPartial:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user