Skip to main content

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-node installs 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/sentencepiece or a maintained alternative; verify and document in DECISIONS.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 offsets field 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 raw with 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 infer call, not on construction (unless warmup: 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-T starts 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-T continues 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 was B-T or I-T, else treat as B-T (graceful)
    • O ends 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 Classifier from @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):
    1. Tokenize the section's text
    2. 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.
    3. 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.
    4. Filter by minConfidence
    5. 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 Span objects 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)
    6. Emit ClassificationProposal[] with source: 'neural', source_id from 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 LocaleProfile has a weightsPackage, instantiate a NeuralSequenceClassifier for 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.apply before solving
  • 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: countryneural_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: from rule_only to neural_preferred with confidence_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 country accuracy 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
    • source fields are correctly populated
  • 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, the LocaleProfile extension point
  • Update root README.md with 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 publish each. Use --access public for scoped packages.
  • Verify install in a clean directory: npm install @mailwoman/neural @mailwoman/neural-weights-en-us and 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 parse works with --locale and 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.