Phase 4.3 — Resolver Integration
Parent: PHASE_4_resolver.md.
Predecessor: PHASE_4_2_wof_sqlite.md.
Goal: wire @mailwoman/resolver-wof-sqlite into the parser pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-gate → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package.. After Phase 4.3 a user can run mailwoman parse --resolve --format xml "75004 Paris" and get back an <address> tree whose <locality> carries src="wof-admin:101751119", lat="48.85", lon="2.34" — the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. overlays its attribution on top of the classifier-derived provenance from PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.1.
Branch: TBD when this phasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). begins. Probably broken into 4.3.a (output-shape changes) and 4.3.b (pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-gate → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package. wiring) for reviewability.
Depends on: PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.2 (@mailwoman/resolver-wof-sqlite published) + PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.1 (src attribute on AddressNode).
Status
Shipped (2026-05-20). Implementation matches the design below, with three deviations called out in the changelog. The Resolver interface (type contract) lives in @mailwoman/core/resolver; resolveTree + createWOFResolver moved to the root @mailwoman/resolver package in #215 (so the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. can depend on @mailwoman/spatial + @mailwoman/codex while @mailwoman/core stays a leaf). The WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. backing is provided through @mailwoman/resolver-wof-sqlite as an optional peer dep that the CLI dynamic-imports only when --resolve is set.
What "integration" means here
PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.2 ships a self-contained PlaceLookup that takes a text query and returns ranked candidates. PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.3 turns that into something the parser pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-gate → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package. knows how to use:
- A
Resolverinterface that takes a parsedAddressTreeand decorates it in-place with resolved place IDs + coordinates. - A wiring point in the parser where the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. runs after the decoderdecoderIn a transformer encoder-decoder model, the part that produces output sequences. Mailwoman's classifier is encoder-only (no decoder); the 'CRF decoder' is a different thing — a structured-prediction layer that picks the best label sequence from the encoder's outputs. produces the tree.
- A CLI flag (
--resolve) that toggles the pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-gate → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package. on.
The decorated AddressTree flows through the existing XML/JSON/tuple projections — PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.1's src attribute already carries the right shape; the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. simply overlays its own source + sourceId on the nodes it resolved, displacing the classifier-derived attribution into metadata for debugging.
Public API sketch
// mailwoman/resolver.ts (new file in the user-facing workspace)
import type { AddressTree } from "@mailwoman/core/decoder"
import type { PlaceLookup } from "@mailwoman/resolver-wof-sqlite"
export interface ResolveOpts {
/** Hard limit on how many resolver lookups one tree is allowed to issue. Default 10. */
maxLookups?: number
/**
* Minimum candidate score for resolver attribution to win over classifier attribution. Default
* 0.5.
*/
minWinningScore?: number
}
export interface Resolver {
/** Walk the tree top-down, resolve each node where possible, return a new tree with decorations. */
resolveTree(tree: AddressTree, opts?: ResolveOpts): Promise<AddressTree>
}
export function createWOFResolver(lookup: PlaceLookup): Resolver
Decoration shape
When the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. successfully matches a node:
| Field | Before resolve | After resolve |
|---|---|---|
AddressNode.source | "rule" or "neural" (classifier) | "resolver" |
AddressNode.sourceId | classifier id ("whos_on_first", "neural-v0.3.1-en-us") | "wof-admin:<wof_id>" |
AddressNode.lat (new) | undefined | resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon.-supplied |
AddressNode.lon (new) | undefined | resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon.-supplied |
AddressNode.placeId (new) | undefined | normalized URI ("wof:101751119") |
AddressNode.metadata.classifier_source (new) | undefined | the displaced classifier source |
AddressNode.metadata.classifier_source_id (new) | undefined | the displaced classifier sourceId |
When the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. does NOT match a node (no candidate above minWinningScore, or no resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. for the placetypeplacetypeThe Who's On First hierarchical classification of places: planet → continent → country → region → county → locality → neighbourhood. The resolver uses placetype to rank candidates — an exact locality match outranks a county-level match.), the node's existing classifier attribution is preserved unchanged.
XML output (post-resolve)
<address raw="75004 Paris">
<locality start="6" end="11" conf="0.94" src="wof-admin:101751119" lat="48.8534" lon="2.3488">
Paris
<postcode start="0" end="5" conf="0.99" src="rule:postcode">75004</postcode>
</locality>
</address>
postcode keeps its classifier-derived src because the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. doesn't ship a postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. lookup yet (would need the postalcode WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. shardshardA partial output file of the corpus build, written in Parquet format. The training pipeline streams shards row by row. — PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.3.x follow-up).
Walk strategy
The resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. walks top-down with parent-constraint inheritance:
- Resolve the root (typically
countryorlocalityif no countrycountryThe top-level address component (an ISO country). Closed-vocabulary, so it is best handled by a deterministic matcher feeding a proposal rather than a retrained model head. was extracted). - For each child, query with
parentId = parent.placeId.wof_idif the parent resolved. This narrows the search space dramatically —Springfieldunder Illinois resolves correctly without countrycountryThe top-level address component (an ISO country). Closed-vocabulary, so it is best handled by a deterministic matcher feeding a proposal rather than a retrained model head./regionregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality. disambiguation. - If the parent didn't resolve, fall back to unconstrained query.
Bounded by maxLookups — a tree with 20 candidate nodes won't trigger 20 queries; the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. gives up at the limit and leaves the remaining nodes with classifier attribution.
CLI
mailwoman parse --resolve --format xml "75004 Paris, FR"
mailwoman parse --resolve --resolve-db /path/to/wof.db --format xml "..."
--resolveenables the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon.. Off by default — the rest of the pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-gate → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package. is unchanged.--resolve-db <path>overrides the default WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. DB path. The default reads from$MAILWOMAN_WOF_DBenv, else errors with a clear message about where to get the WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. distribution + how to set the env.- Adds ~50–100ms per parseaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates. for the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. step (a handful of FTS5FTS5SQLite's built-in full-text-search module (with BM25 ranking). Mailwoman uses it for prefix and name matching against the gazetteer. queries). Acceptable for the CLI use case; library users can disable per-call.
What's NOT in Phase 4.3
- PostcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. resolution — separate
wof-postalcodeshardshardA partial output file of the corpus build, written in Parquet format. The training pipeline streams shards row by row., opt-in via--resolve-postcodes(PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.3.x). - StreetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels.-level / address-level resolution — WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. doesn't go that deep; would need OSMOpenStreetMap (OSM). A community-curated global map database (ODbL-licensed) with addr:* tagged features and place hierarchies. A secondary corpus source and a source of street names. / OpenAddressesOpenAddresses (OA). A global open aggregation of address points collected from many official sources. A primary source of component-supervised training data outside proprietary registries. gazetteersgazetteerA geographical index that maps place names and postcodes to real-world coordinates. Mailwoman uses a custom-built Who's On First (WOF) SQLite database as its gazetteer — the 'atlas' half of the grammar/atlas architecture., license-checked. PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.4 candidate.
- ResolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. feedback into parsingaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates. (the loop where resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. disagreement triggers re-classification) — PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.4+.
- Multiple resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. implementations composed in priority order — for v1, one
Resolverper parseaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates.. Composition lands when there's a second resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. to compose with.
Tests
- UnitunitA subdivision of a building — apartment, suite, floor — that refines a street address. Mailwoman's unit component; a designator plus identifier forms a subpremise.:
resolveTreeagainst a tree + a fakePlaceLookupthat returns canned candidates. Cover: full match, partial match, no match, parent-constrained child lookup,maxLookupsbudget. - Integration:
mailwoman parse --resolve --format xmlagainst a known-good WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. DB. Pinned outputs for ~6 well-known addresses (Paris,FR; Paris,TX; Springfield,IL; etc.). - The CLI integration tests gate on the WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. DB being present (skip-if-missing pattern, matching how the localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for.-flag tests handle their fixture deps).
Open questions
- Where does the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. live in the workspace tree? Two candidates:
mailwoman/resolver.ts(in the user-facing workspace) — keeps the wiring close to the CLI and parser callsite.@mailwoman/core/resolver(new subpath) — better if a third-party adapter (e.g. BANBAN (Base Adresse Nationale). France's authoritative open national address register — the highest-quality training source for French addresses, with full component structure.-API) wants the same interface.- Lean toward
core/resolver— interface separation pays off as soon as PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.4'sRemoteResolverlands.
- Lat/lon precisionprecisionOf the spans the model labeled as a given tag, the fraction it got right. High precision means few false positives. Paired with recall to compute F1.. WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. stores ~4–6 decimal places; some downstream consumers want bounding boxes instead of centroids. Defer: emit centroids, document that
geom:bboxis available viaAddressNode.metadata.wof_bboxif needed. - ResolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. caching. A common parseaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates. stream (
Paris, FRtyped by 1000 users) shouldn't issue 1000 FTS queries. Defer: in-process LRU at the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. level, sized bymaxCacheSizeopt (default 1000). Test once PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.3 is exercised in a real workload.
Open dependencies
- WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. data download still needs operator authorization (carried over from the PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.2 handover). Without the actual DB on disk, the CLI integration tests can't run end-to-end.
Changelog
- 2026-05-20 — written as design intent during the autonomous night shiftnight shiftAn autonomous overnight agent session — training launches, evals, publishing, issue triage — that ends with a structured postmortem (what shipped, regressions, open questions) committed for handoff.. Not yet started; awaits operator review.
- 2026-05-20 (PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.3.x — proximity ranking) —
FindPlaceQuerygainsnear(soft boost or hard filter viamaxDistanceKm) andbbox(hard filter). Backed by an RTree virtual table built alongside FTS5FTS5SQLite's built-in full-text-search module (with BM25 ranking). Mailwoman uses it for prefix and name matching against the gazetteer. bymailwoman-wof-build-fts. No new deps — both FTS5FTS5SQLite's built-in full-text-search module (with BM25 ranking). Mailwoman uses it for prefix and name matching against the gazetteer. and RTree ship with core SQLite. Closes the "no popularity signal" gap from PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.2 by letting callers express "I'm here, find what's near me" or "find places within this regionregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality." without SpatiaLite. Backwards-compatible: DBs built before this PR keep working; bbox filter is silently dropped when the R*TreeR*TreeSQLite's spatial index of bounding boxes, enabling fast geographic range and nearest-neighbour queries in the resolver. is missing, proximity boost still works via centroid columns directly. 13 new unitunitA subdivision of a building — apartment, suite, floor — that refines a street address. Mailwoman's unit component; a designator plus identifier forms a subpremise. tests + smoke against real WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. (142,383 bbox rows indexed in 3.14s alongside the FTS5FTS5SQLite's built-in full-text-search module (with BM25 ranking). Mailwoman uses it for prefix and name matching against the gazetteer. build). - 2026-05-20 (same day, post-sync) — shipped. Three deviations from the original plan:
@mailwoman/resolver-wof-sqliteis an optional peer dep, not a hard dep. The CLI dynamic-imports it insidewithResolver()so callers who never set--resolvedon't pay the kysely + resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. bundle cost. Honors the PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.2 plan's "optional dep" intent.PlaceCandidate.wof_id→idrename. PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.2 shipped the WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. candidate with awof_idfield; that was structurally incompatible withcore/resolver's genericResolvedPlace.id. Renamed in this PR soWofSqlitePlaceLookupsatisfiesResolverBackendwithout an adapter shim. No external consumers yet so the breakage was free.- Pre-existing localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for.-casing bug fix bundled.
resolveWeightswas forming@mailwoman/neural-weights-en-US(uppercase regionregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality.) but the package is lowercase. The CLI accepts canonicalen-UScasing;weights.tsnow lowercases before forming the package name. Surfaced on the first end-to-end smoke testverdict-smokeA short diagnostic training run (≈1,500–3,000 steps) that must match the full run's gradient-noise profile, used to catch recipe instability before committing to a full 50k-step launch.; fix shipped alongside PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4.3.