Phase 3 — TypeScript Integration & Ship
Goal: integrate the trained 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 Mailwoman via NeuralSequenceClassifier, wire it through the policy system, publish @mailwoman/neural@0.1.0 and 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. packages to npm. End stateregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality.: a user can npm install @mailwoman/neural @mailwoman/neural-weights-en-us and get neural classification for coarse components.
Duration estimate: 1.5 weeks.
Branch: neural/phase-3-integration
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). 2 complete with 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. packages on disk.
Pre-flight
- PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 2 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. packages exist and pass evalevalRunning the model against a held-out golden dataset and computing per-component F1, exact-match, calibration, and resolved-coordinate error.
- You have npm publish access (or know how to coordinate with the maintainer for publishing)
-
onnxruntime-nodeinstalls cleanly in the dev environment
Tasks
1. Package scaffolding
- Create
packages/neural/workspace - Dependencies:
onnxruntime-node, SentencePieceSentencePieceA language-independent subword tokenizer that splits text into pieces using a unigram language model. Mailwoman uses a SentencePiece tokenizer with a 48,000-token vocabulary and byte-fallback, trained on address data rather than general text. WASM (pick one —@bloomberg/sentencepieceor a maintained alternative; verify and document inDECISIONS.md),@mailwoman/core - Strict TypeScript config
2. SentencePiece tokenizer wrapper
-
packages/neural/src/tokenizer.ts - Loads the SentencePieceSentencePieceA language-independent subword tokenizer that splits text into pieces using a unigram language model. Mailwoman uses a SentencePiece tokenizer with a 48,000-token vocabulary and byte-fallback, trained on address data rather than general text. 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.' from a 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
- Exposes
encode(text: string): { ids: number[]; tokens: string[]; offsets: [number, number][] } - The
offsetsfield is critical — it's how we map 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.' output BIO labelsBIO tagging (Begin-Inside-Outside). A token-level labeling scheme where each token is tagged as B-X (beginning of an entity of type X), I-X (inside an entity of type X), or O (outside any entity). Mailwoman uses BIO over SentencePiece tokens to annotate address components. back to character spansspanA contiguous range of characters or tokens in the input string, tagged with an address component type (street, locality, postcode, etc.). Parsed addresses are represented as collections of spans, possibly nested in a tree. in the original input.
⚠ TokenizertokenizerThe component that converts a raw address string into a sequence of numeric token IDs the model can process. Mailwoman's tokenizer is a SentencePiece unigram model trained specifically on postal addresses. parity check — the single most important test in this phasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary).:
-
packages/neural/test/tokenizer-parity.test.ts - Loads 10k
(raw, tokens)pairs from corpuscorpusThe BIO-labeled training data used to train Mailwoman's neural classifier. Assembled from real sources (OpenAddresses, National Address Database) and synthetic shards (boundary stress, order variants, negative space). Managed by @mailwoman/corpus.-v0.1.0 - Re-tokenizes each
rawwith the TS SentencePieceSentencePieceA language-independent subword tokenizer that splits text into pieces using a unigram language model. Mailwoman uses a SentencePiece tokenizer with a 48,000-token vocabulary and byte-fallback, trained on address data rather than general text. - Asserts byte-for-byte equality with the stored
tokens - If a single example fails, the integration is broken. Fix before continuing.
3. ONNX inference wrapper
-
packages/neural/src/onnx-runner.ts - Loads ONNXONNX (Open Neural Network Exchange). An open format for machine learning models that enables interoperability between training frameworks and inference runtimes. Mailwoman ships its trained model as an ONNX file so it can run in Node.js and the browser via onnxruntime. 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.' via
onnxruntime-node - Exposes
infer(tokenIds: number[]): Promise<number[][]>returning logitslogitA raw, unnormalized per-label score the model outputs before softmax. Priors and biases are added in logit space, then softmax turns logits into probabilities. per tokentokenOne word or subword in the tokenized input. For the neural classifier, tokens come from SentencePiece (subword units); for the rule classifiers, tokens are whitespace- and punctuation-separated words. - Handles batching (group multiple sections for one inferenceinferenceRunning the trained model on new input to get predictions, as opposed to training, which produces the model. In Mailwoman that means a small transformer encoder reads an address string and classifies every token — house number, street, locality, region, postcode, and the rest. A Who's On First gazetteer can feed soft location hints into the pass, but the model makes the final call on every label. Where a generative model writes text token by token, Mailwoman's output is a retrieval-augmented token classification: one label per input piece. call)
- Configurable execution provider (CPU default, CUDA via env var)
- Lazy loading: 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.' loads on first
infercall, not on construction (unlesswarmup: true)
4. BIO decoding
-
packages/neural/src/decode.ts - Input: logitslogitA raw, unnormalized per-label score the model outputs before softmax. Priors and biases are added in logit space, then softmax turns logits into probabilities. per tokentokenOne word or subword in the tokenized input. For the neural classifier, tokens come from SentencePiece (subword units); for the rule classifiers, tokens are whitespace- and punctuation-separated words., original tokentokenOne word or subword in the tokenized input. For the neural classifier, tokens come from SentencePiece (subword units); for the rule classifiers, tokens are whitespace- and punctuation-separated words. offsets
- Output: list of
(span, component, confidence)tuples - BIO decoding rules:
B-Tstarts a new spanspanA contiguous range of characters or tokens in the input string, tagged with an address component type (street, locality, postcode, etc.). Parsed addresses are represented as collections of spans, possibly nested in a tree.I-Tcontinues the previous spanspanA contiguous range of characters or tokens in the input string, tagged with an address component type (street, locality, postcode, etc.). Parsed addresses are represented as collections of spans, possibly nested in a tree. if it wasB-TorI-T, else treat asB-T(graceful)Oends any open spanspanA contiguous range of characters or tokens in the input string, tagged with an address component type (street, locality, postcode, etc.). Parsed addresses are represented as collections of spans, possibly nested in a tree.
- Confidence per spanspanA contiguous range of characters or tokens in the input string, tagged with an address component type (street, locality, postcode, etc.). Parsed addresses are represented as collections of spans, possibly nested in a tree. = mean 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. probability of the predicted labelcomponent tagOne of the 33 labels in Mailwoman's address schema — street, locality, region, postcode, house_number, unit, po_box, country, venue, intersection, and others. Each parsed span carries exactly one component tag. across the spanspanA contiguous range of characters or tokens in the input string, tagged with an address component type (street, locality, postcode, etc.). Parsed addresses are represented as collections of spans, possibly nested in a tree.'s tokenstokenOne word or subword in the tokenized input. For the neural classifier, tokens come from SentencePiece (subword units); for the rule classifiers, tokens are whitespace- and punctuation-separated words.
- Test cases: well-formed BIO, ill-formed BIO (recovery), all-O input, single-tokentokenOne word or subword in the tokenized input. For the neural classifier, tokens come from SentencePiece (subword units); for the rule classifiers, tokens are whitespace- and punctuation-separated words. spansspanA contiguous range of characters or tokens in the input string, tagged with an address component type (street, locality, postcode, etc.). Parsed addresses are represented as collections of spans, possibly nested in a tree.
5. NeuralSequenceClassifier
-
packages/neural/src/sequence-classifier.ts - Implements
Classifierfrom@mailwoman/core - Constructor takes
NeuralSequenceClassifierOptions, resolves and loads the 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 -
classify(section, context):- Tokenize the section's text
- Run ONNXONNX (Open Neural Network Exchange). An open format for machine learning models that enables interoperability between training frameworks and inference runtimes. Mailwoman ships its trained model as an ONNX file so it can run in Node.js and the browser via onnxruntime. inferenceinferenceRunning the trained model on new input to get predictions, as opposed to training, which produces the model. In Mailwoman that means a small transformer encoder reads an address string and classifies every token — house number, street, locality, region, postcode, and the rest. A Who's On First gazetteer can feed soft location hints into the pass, but the model makes the final call on every label. Where a generative model writes text token by token, Mailwoman's output is a retrieval-augmented token classification: one label per input piece.
- Decode BIO labelsBIO tagging (Begin-Inside-Outside). A token-level labeling scheme where each token is tagged as B-X (beginning of an entity of type X), I-X (inside an entity of type X), or O (outside any entity). Mailwoman uses BIO over SentencePiece tokens to annotate address components. to spansspanA contiguous range of characters or tokens in the input string, tagged with an address component type (street, locality, postcode, etc.). Parsed addresses are represented as collections of spans, possibly nested in a tree.
- Filter by
minConfidence - Map spansspanA contiguous range of characters or tokens in the input string, tagged with an address component type (street, locality, postcode, etc.). Parsed addresses are represented as collections of spans, possibly nested in a tree. back to
Spanobjects in the original input (use SentencePieceSentencePieceA language-independent subword tokenizer that splits text into pieces using a unigram language model. Mailwoman uses a SentencePiece tokenizer with a 48,000-token vocabulary and byte-fallback, trained on address data rather than general text. offsets, then translate to original-input offsets via section's start offset) - Emit
ClassificationProposal[]withsource: 'neural',source_idfrom the model cardmodel cardA JSON metadata file (model-card.json) shipped with each weights bundle. It declares the model version, lineage, label set, required inference channels (anchor, gazetteer), calibration data, and training provenance. version
- Returns empty array if 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.' fails to load (graceful degradation per
reference/OPERATIONS.md)
6. Weights package loader
-
packages/neural/src/weights.ts -
loadWeights(packageName: string): Promise<{ modelBytes, tokenizerBytes, modelCard }> - Resolves package via Node module resolution
- Reads files from package's distribution directory
- Caches in memory across instances of
NeuralSequenceClassifier
7. Solver integration
- In
packages/core/src/solver.ts(or wherever the existing solver entry lives), add the neural classifierneural 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.' path:- If a
LocaleProfilehas aweightsPackage, instantiate aNeuralSequenceClassifierfor that localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for. - Run it alongside rule classifiers during the classify phasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary).
- All proposals flow through
PolicyRegistry.applybefore solving
- If a
- No solver logic changes needed — the abstraction from PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 0 was designed for this.
8. Policy migration: country → neural_preferred
The first Ship of TheseusShip of TheseusThe migration pattern Mailwoman uses: replace rule classifiers with the neural classifier one component at a time, only when metrics justify it. Named for the philosophical thought experiment. step.
- Update
packages/core/src/policy-defaults.ts:country: fromrule_onlytoneural_preferredwithconfidence_threshold: 0.8
- Run the full test suite + golden setgolden setA hand-labeled evaluation dataset of US, French, and adversarial addresses used as ground truth for evals. evalevalRunning the model against a held-out golden dataset and computing per-component F1, exact-match, calibration, and resolved-coordinate error.
- Verify on the golden setgolden setA hand-labeled evaluation dataset of US, French, and adversarial addresses used as ground truth for evals. that
countryaccuracy improved (or held) and no other component regressed - If anything regresses, revert the policy change and investigate. The migration is gated on metrics, not on intent.
▶ Repeat the same for region if PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 2 metrics justify. Coarse components only. Do not migrate locality or below in PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 3 — defer until TiertierInternal versioning of which label classes the model emits. Tier 1 is the coarse components (country, region, locality, postcode); Tier 2 adds venue, street, house_number; Tier 3 (future) would add attention, po_box, and POI venue subtyping. Historically called 'Stage 1/2/3' before the runtime-pipeline naming made that ambiguous. 2 trains.
9. End-to-end test
-
packages/neural/test/e2e.test.ts - Test:
parse("123 Main St, Portland, OR 97215", { locale: 'en-US' })- Neural classifierneural 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.' emits
country: USA(inferred even though "USA" is absent from input) - Rule classifier still emits
postcode: 97215 - Final solution has both
sourcefields are correctly populated
- Neural classifierneural 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.' emits
- Test: same with
locale: 'fr-FR'and a French address - Test: missing 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 → graceful degradation, rule-only solution
10. CLI surfacing
-
npx mailwoman parse --locale en-US --neural ...opts into neural classifierneural 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.' -
npx mailwoman parse --locale en-US --no-neural ...opts out (rule-only) - Default: neural on if 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 is installed, off otherwise
- Debug output: when neural is on, show which classifier produced each component in the output
11. Documentation
-
packages/neural/README.md— installation, usage, configuration, theLocaleProfileextension point - Update root
README.mdwith a new "Neural" section - Migration guide: how an existing Mailwoman user opts into the neural classifierneural 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.'
- Performance numbers from the benchmark suite
12. Performance & benchmarks
-
packages/neural/bench/with vitest-bench or simple scripts - Benchmark: cold load time, warm classification latency p50/p95/p99, throughput, memory footprint
- All within budgets in
reference/OPERATIONS.md - If neural is slower than budgeted: profile. Likely culprits: ONNXONNX (Open Neural Network Exchange). An open format for machine learning models that enables interoperability between training frameworks and inference runtimes. Mailwoman ships its trained model as an ONNX file so it can run in Node.js and the browser via onnxruntime. session not reused across calls, batching not enabled, SentencePieceSentencePieceA language-independent subword tokenizer that splits text into pieces using a unigram language model. Mailwoman uses a SentencePiece tokenizer with a 48,000-token vocabulary and byte-fallback, trained on address data rather than general text. WASM not warm.
13. Publish
- Final version bumps:
@mailwoman/core→ minor bump (added types)@mailwoman/classifiers→ minor bump (adapter additions)@mailwoman/neural@0.1.0→ new, marked beta@mailwoman/neural-weights-en-us@0.1.0→ new, marked beta@mailwoman/neural-weights-fr-fr@0.1.0→ new, marked beta
- Check licenses on every package's
package.json— AGPL-3.0 for code, 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. packages need their own license decision (see PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 6 license note) -
npm publisheach. Use--access publicfor scoped packages. - Verify install in a clean directory:
npm install @mailwoman/neural @mailwoman/neural-weights-en-usand run a sample 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..
⚠ Before publishing 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., settle the license question. 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. is ODbL (share-alike). 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. is CC0. BANBAN (Base Adresse Nationale). France's authoritative open national address register — the highest-quality training source for French addresses, with full component structure. is Licence Ouverte. A 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.' trained on ODbL data is arguably ODbL — talk to a lawyer or pick a clear license posture and document it.
Success criteria checklist
- TokenizertokenizerThe component that converts a raw address string into a sequence of numeric token IDs the model can process. Mailwoman's tokenizer is a SentencePiece unigram model trained specifically on postal addresses. parity test green on 10k examples
- End-to-end test green for en-US and fr-FR
- Benchmarks within budget
- Golden setgolden setA hand-labeled evaluation dataset of US, French, and adversarial addresses used as ground truth for evals.: 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. F1 improved (or held), nothing regressed
-
npx mailwoman parseworks with--localeand uses neural classifierneural 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.' when available - Packages published to npm
- Documentation updated
- Branch tagged
neural-phase-3-complete - Blog post draft (optional but recommended) in
docspathname:///research/2026-...-neural-classifier.md
When the human will want a check-in
After Phase 3 ships, pause. The human (the project creator) should look at:
- Real-world feedback from a couple of test deployments
- The blog post draft
- License decisions on 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. packages
- Whether to proceed to TiertierInternal versioning of which label classes the model emits. Tier 1 is the coarse components (country, region, locality, postcode); Tier 2 adds venue, street, house_number; Tier 3 (future) would add attention, po_box, and POI venue subtyping. Historically called 'Stage 1/2/3' before the runtime-pipeline naming made that ambiguous. 2 (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) or pause to gather usage data
Do not begin PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 4 (geocoder fusion) or even TiertierInternal versioning of which label classes the model emits. Tier 1 is the coarse components (country, region, locality, postcode); Tier 2 adds venue, street, house_number; Tier 3 (future) would add attention, po_box, and POI venue subtyping. Historically called 'Stage 1/2/3' before the runtime-pipeline naming made that ambiguous. 2 of PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 1/2 (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 trainingtrainingThe process of adjusting a model's parameters so its predictions match labeled examples, by repeatedly measuring error and nudging the weights to reduce it. Distinct from inference, when the trained model is run on new input.) without explicit confirmation. The point of shipping is to learn.
When to call this phase done
When npm install @mailwoman/neural @mailwoman/neural-weights-en-us followed by a parse() call works on a clean machine and produces output with source: 'neural' proposals for the country component.