Skip to main content

Coarse-placer as a soft country prior — wiring spec (#244)

Wire the OA-broadened coarse-placercoarse-placerA lightweight int8 country classifier (~0.79 MB) that predicts which of a set of target countries an address belongs to, feeding a soft prior into resolver disambiguation. (the promoted #244 modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.') into 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. as a soft 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. prior — informs ranking, never filters — at an abstention threshold of ~0.9. Default-off, byte-stable, PR-and-flag. The verdict that motivates the soft-signal-at-0.9 choice: docs/articles/evals/2026-06-14-coarse-placer-oa-breadth.md.

Goal & non-goals

  • Goal: when the geocoder resolves an address, let the coarse-placercoarse-placerA lightweight int8 country classifier (~0.79 MB) that predicts which of a set of target countries an address belongs to, feeding a soft prior into resolver disambiguation.'s whole-string 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. guess bias 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.-level disambiguation (regionregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality. + localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. namesake collisions), the same way 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. pins it today — so Plauen, … (DE) stops resolving to a US/IT namesake, and an off-map address stops getting pinned to a wrong in-map 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..
  • Non-goal: it is not a gate. It never filters candidates, never abstains 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., never overrides an explicit --default-country/localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for. or a stronger signal. A wrong guess costs a little ranking weightparameterA single learned number inside a model — one weight or bias. Mailwoman's encoder has roughly 30 million of them; training is the search for good values., never a dropped or wrong-filtered result. (This is why ~0.9 is safe — see Threshold.)

The mechanism already exists — reuse the #369 anchor re-rank

The 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.-anchor re-rank (#369, core/resolver/resolve.ts) is exactly the soft-prior machinery:

anchorPosterior: Record<country, prob> // a country→probability map
re-rank: sort by exactMatch (PRIMARY, tier-safe) THEN score + anchorWeight * posterior[candidate.country]
→ byte-identical when anchorPosterior is undefined; applied to region + locality only

So the coarse-placercoarse-placerA lightweight int8 country classifier (~0.79 MB) that predicts which of a set of target countries an address belongs to, feeding a soft prior into resolver disambiguation. becomes another anchorPosterior source — no new ranking code. CoarsePlacer.predict(text) returns { country, confidence, abstained }; we map a non-abstained prediction to a one-hot-ish posterior { [country]: confidence } (or a softmaxsoftmaxThe function that converts a vector of logits into a probability distribution summing to 1, applied after priors and biases are added to the emission logits. over the top-k) and feed it through the existing re-rank.

Composition with the postcode anchor (precedence)

Both emit country posteriorscountry posteriorA country → probability map (derived from postcodes or the coarse-placer) that re-ranks resolver candidates as a soft prior, never a hard filter.; they must not double-count.

  • Postcode anchoranchor inferenceA technique where structured knowledge (postcode locations, gazetteer place names) is injected into the model as soft input features — not as deterministic overrides. The model still decides the final labels, but the anchor signal biases it toward correct admin tags. present → it wins. 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. pins the 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. far more reliably than a whole-string guess. The coarse-placercoarse-placerA lightweight int8 country classifier (~0.79 MB) that predicts which of a set of target countries an address belongs to, feeding a soft prior into resolver disambiguation. posterior is not applied when the #369 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. posterior exists.
  • No postcode anchoranchor inferenceA technique where structured knowledge (postcode locations, gazetteer place names) is injected into the model as soft input features — not as deterministic overrides. The model still decides the final labels, but the anchor signal biases it toward correct admin tags.coarse-placercoarse-placerA lightweight int8 country classifier (~0.79 MB) that predicts which of a set of target countries an address belongs to, feeding a soft prior into resolver disambiguation. fills the gap, at a lower anchorWeight than the 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. anchor's 2.0 (proposed 1.0) — it's a broader, softer signal, so it should blend more gently with score.
  • Coarse-placercoarse-placerA lightweight int8 country classifier (~0.79 MB) that predicts which of a set of target countries an address belongs to, feeding a soft prior into resolver disambiguation. says OTHER (off-map) → emit no in-map posteriorposterior (country posterior)A per-country probability map — e.g. {GB: 0.8, FR: 0.06} — expressing how likely an input belongs to each country, produced by a model (the coarse placer's softmax, or a postcode anchor's lookup) AFTER seeing the input (hence 'posterior', from Bayesian usage: the updated belief). The resolver consumes it as a soft ranking boost (anchorPosterior): each candidate's score gains weight × posterior[candidate.country]. Values lie in [0, 1] and need not sum to 1 (in-map marginals exclude the OTHER class). (equivalently a flat/empty map): don't boost any of the 11; let the unconstrained ranking handle it. That's the graceful off-map path.

Where it runs

  • New opt-in 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. stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale gate, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). placeCountry (early — StagestageOne of the dataflow stages in the runtime pipeline (normalize, locale gate, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). ~2, on the normalized string), mirroring how classifier/resolver/fst are optional stagesstageOne of the dataflow stages in the runtime pipeline (normalize, locale gate, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). in createRuntimePipeline. When provided, 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. computes the posterior once and threadsthreadA parallel workstream within a release. Threads compose; they are not sequential milestones like phases. it into resolveOpts.anchorPosterior/anchorWeight (deferring to 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. posterior per the precedence rule above).
  • geocode-core wires the same: a CoarsePlacer dep (optional), posterior fed into the resolve opts.
  • Off by default → byte-identical. Like resolve/fst, absent the stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale gate, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). 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. PR-and-flag; promotion to default is a separate, evidence-gated step.

Threshold ~0.9 (and why the soft framing earns it)

abstainBelow = 0.9 (vs the code default 0.5). Below 0.9 → abstainedno posterior → byte-stable.

The measured tradeoff (OA-retrained modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.', docs/articles/evals/2026-06-14-coarse-placer-oa-breadth.md):

abstainin-map accuracyoff-map heldout caught
0.5095.1%66.1%
0.8591.8%82.5%
0.9587.8%87.6%

The in-map "cost" of a high threshold is a false abstention — but in the soft design a false abstention just means "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. hint this time," which degrades gracefully (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. ranks unconstrained, still correct). The harmful error is the opposite: a confident wrong placement on an off-map address (→ wrong 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. search). So the asymmetry favors a high threshold, and ~0.9 sits where off-map catch is high (~85%) while the only thing we "lose" on in-map is some boosts we'd have applied — not correctness. (A hard-gate design would force ~0.85 to keep in-map routing >90%; the soft design removes that pressure.)

Validation — grade the ASSEMBLED pipeline, not the model

Per the reconcile-retirement lesson (grade 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. against truth, never a component's intrinsic F1):

  • Primary metric: on a 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.-disambiguation evalevalRunning the model against a held-out golden dataset and computing per-component F1, exact-match, calibration, and resolved-coordinate error. (ambiguous namesakes — Berlin DE/US, Plauen, bare regionregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality. abbreviations "VT"/"ME" — plus off-map addresses), measure the geocoder's right-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. rate WITH vs WITHOUT the coarse-placercoarse-placerA lightweight int8 country classifier (~0.79 MB) that predicts which of a set of target countries an address belongs to, feeding a soft prior into resolver disambiguation. posterior. Must improve the ambiguous/off-map cases at no in-map regression (reuse the honest-evalevalRunning the model against a held-out golden dataset and computing per-component F1, exact-match, calibration, and resolved-coordinate error. harness + the #369 namesake set).
  • Byte-stability check: with the stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale gate, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). off, output is byte-identical (CI-assertable).
  • Gate promotion-to-default on a measured netneural networkA model made of layers of simple numeric units whose connection strengths (weights) are learned from data. The transformer encoder at Mailwoman's core is a neural network.-positive here, exactly as #584/#590 were gated.

Prerequisite — the model must ship

The coarse-placercoarse-placerA lightweight int8 country classifier (~0.79 MB) that predicts which of a set of target countries an address belongs to, feeding a soft prior into resolver disambiguation. int8 modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.' (0.79 MB) currently lives only on $MAILWOMAN_DATA_ROOT; nothing ships it. Wiring it for installed consumers needs it packaged. Recommended: commit the int8 artifact under core/data/ and add it to @mailwoman/core's files (it already ships ~9 MB of dictionaries; +0.79 MB is negligible), with CoarsePlacer.fromArtifactDir resolving the bundled path by default + an env/opt override. (Alternative: a dedicated weightsparameterA single learned number inside a model — one weight or bias. Mailwoman's encoder has roughly 30 million of them; training is the search for good values. package — heavier process for a tiny artifact.) This is gated by the v4.8.1 clean-install 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. — the new artifact must resolve from a fresh npm install.

Use-case impact (the three modes)

  • CLI (mailwoman geocode/parse): opt-in flag (--place-country / on when the modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.' is present). The placer is always-resident + microsecond-cheap, so no startup cost concern.
  • Library (import "mailwoman" batchbatch sizeHow many examples the model processes before each parameter update. Larger batches give smoother gradients but cost more memory; gradient accumulation simulates a big batch on a small GPU. geocodinggeocodingThe process of converting an address into geographic coordinates (latitude and longitude). Mailwoman geocodes in a multi-tier cascade: exact address-point match → street interpolation → locality centroid. Each tier is progressively coarser but more widely available.): a CoarsePlacer dep on createRuntimePipeline / geocodeAddress; callers opt in. Most valuable here (record-matching/normalization at scale benefits from correct 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. disambiguation).
  • Client / Docusaurus (static assets): the modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.' is a 0.79 MB int8 linear classifier with a pure char-ngram featurizer — it can run in-browser, but that's a follow-on web build (a @mailwoman/… browser export + bundling the artifact). Not in this spec's scope; flagged as the natural next step so the demo's "it all runs client-side" story eventually includes 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. routing.

Risks & the open lever

  • Double-counting with the postcode anchoranchor inferenceA technique where structured knowledge (postcode locations, gazetteer place names) is injected into the model as soft input features — not as deterministic overrides. The model still decides the final labels, but the anchor signal biases it toward correct admin tags. → the precedence rule (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. wins; coarse-placercoarse-placerA lightweight int8 country classifier (~0.79 MB) that predicts which of a set of target countries an address belongs to, feeding a soft prior into resolver disambiguation. fills).
  • Modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.'-not-shipped → the packaging prerequisite above; must pass ci:smoke.
  • The linear ceiling → the threshold can't push both axes past ~90 (curves cross at ~88/88). The follow-on (separate milestone) is an open-set / novelty method (Mahalanobis on the in-map manifold, or a "not-any-of-11" headattention headOne of several parallel attention computations in a layer, each free to focus on a different kind of relationship between tokens. Their outputs are concatenated — 'multi-head attention'. Mailwoman uses 4 heads.) that moves the whole frontier out — at which point the threshold relaxes and a default-on (even gate) integration becomes defensible.

Phasing

  1. M1 (this spec): ship the int8 modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.' in @mailwoman/core; add the opt-in placeCountry stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale gate, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). feeding anchorPosterior (precedence-aware), threshold 0.9; the assembled-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. 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.-disambiguation evalevalRunning the model against a held-out golden dataset and computing per-component F1, exact-match, calibration, and resolved-coordinate error.; byte-stability + ci:smoke checks. PR-and-flag, default-off.
  2. M2: open-set/novelty method to lift the 90/90 ceiling → consider default-on.
  3. M3: browser build for the client/demo path.