Skip to main content

Install Mailwoman and parse your first address

By the end of this page you'll have a Node script that takes a lowercase, comma-free, unitunitA subdivision of a building — apartment, suite, floor — that refines a street address. Mailwoman's unit component; a designator plus identifier forms a subpremise.-first address and prints it back as a tree of labeled components with a confidence score on each one. No API key, no server, no account. About ten minutes, most of it waiting on the download.

Prerequisites

Read these before you start — two of them will stop you partway through if they don't hold.

  • Node.js 24.18.0 or later. The mailwoman package declares engines.node as >=24.18.0. Check yours with node --version.
  • About 750 MB of free disk. Measured on Linux x64 on 2026-08-03, the three packages below plus their dependencies land 746 MB in node_modules. 500 MB of that is onnxruntime-node's prebuilt binaries and 74 MB is the English 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.' package. This is a one-time cost per project, not 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..
  • An ESM project. The packages are ESM-only. Either put "type": "module" in your package.json or name the script file .mjs, as this page does.

1. Install

npm install mailwoman @mailwoman/neural @mailwoman/neural-weights-en-us

Three packages, three jobs: mailwoman is 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. and the CLI, @mailwoman/neural is the runtime that executes 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.', and @mailwoman/neural-weights-en-us is the English 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.' itself — a 39.4 MB 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. file plus its 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. and lookup tables, 73.8 MB unpacked across 16 files.

Nothing here runs an install script of ours, so nothing phones anywhere during the install beyond npm fetching tarballs.

2. Write the script

Save this as parse.mjs.

import { createRuntimePipeline } from "mailwoman"
import { NeuralAddressClassifier } from "@mailwoman/neural"

const classifier = await NeuralAddressClassifier.loadFromWeights({ locale: "en-US" })
const parse = createRuntimePipeline({ classifier })

const result = await parse("apt 4b 350 5th ave new york ny 10118")

console.log("input:", result.input)
console.log("locale:", result.locale.locale)
console.log("kind:", result.kind.kind)
console.log()

function print(node, depth = 0) {
const pad = " ".repeat(depth)
console.log(`${pad}${node.tag}: "${node.value}" (confidence ${node.confidence.toFixed(2)})`)
for (const child of node.children) print(child, depth + 1)
}

for (const root of result.tree.roots) print(root)

createRuntimePipeline is the entry point to reach for. It wires the whole staged 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. — normalization, structural analysis, localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for. detection, phrase grouping, 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.', and 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. if you give it one — and hands back a single async function. The classifier is the one 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). with no default, because 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 separate download; everything else has a production default already wired.

The address is deliberately awful: lowercase, no commas, and the unitunitA subdivision of a building — apartment, suite, floor — that refines a street address. Mailwoman's unit component; a designator plus identifier forms a subpremise. in front of the house numberhouse numberThe numeric or alphanumeric identifier of a building on a street. Mailwoman's house_number component; its position relative to the street name flips between locales..

3. Run it

node parse.mjs
input: apt 4b 350 5th ave new york ny 10118
locale: en-US
kind: structured_address

region: "NY" (confidence 0.91)
locality: "New York" (confidence 0.93)
street: "5TH" (confidence 0.93)
unit: "Apt 4B" (confidence 0.86)
house_number: "350" (confidence 0.91)
street_suffix: "Ave" (confidence 0.92)
postcode: "10118" (confidence 0.91)

Loading 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.' took 735 ms and the first 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. 402 ms on the machine this page was verified on; the next twenty parsesaddress 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. on the same parse function averaged 7.3 ms each. Build 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. once and keep it, rather than rebuilding it per request.

4. Read the tree

Three things in that output are worth knowing before you write code against it.

It is a tree, not a flat record. result.tree.roots is an array of top-level nodes, and each node has tag, value, confidence, and children. The nesting is geographic containment: NY contains New York, which contains the streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels. and 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., and the streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels. contains the house number, the unitunitA subdivision of a building — apartment, suite, floor — that refines a street address. Mailwoman's unit component; a designator plus identifier forms a subpremise., and the suffix. If you want a flat record, walk the tree and collect by tag — the shape above is what containment looks like, and flattening it is your choice rather than a default the parser makes for you.

The confidence is 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.''s own score, and it is not calibrated by default. Read 0.91 as "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.' scored this 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. 0.91", not as "this 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. is right 91% of the time". A calibration fit ships in the same 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 (calibration.json), and you apply it by wiring createCalibrator from @mailwoman/core/decoder; leaving it out keeps the 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. output byte-stable, which is why it is off by default. The difference is measurable and it runs in one direction: on the held-out set this 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 under-confident, mean confidence 0.913 against 0.980 accuracy, and the fit moves expected calibration error from 0.0698 to 0.0017. If you plan to route on the number — send everything under 0.8 to human review, say — wire the calibrator first, because the uncalibrated score will send you more work than the error rate warrants.

Values come back repaired, not as typed. The input said apt 4b and the output says Apt 4B; the ordinal 5th comes back as 5TH. Hand the same address in mixed case with commas and the repair leaves the casing alone. If you need the exact characters the user typed, keep your own copy of the input string — result.input has it.

Two more fields on result are worth a look when you get there: result.locale records which localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for. 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. decided to run under, and result.kind records what shape of query it thought it had (structured_address here, versus postcode_only, po_box, intersection, and others).

What you have now

A parser running in your own process, with no service dependency, that turns free-text addresses into labeled components with confidence scores you can route on. It does not yet produce coordinates: that needs a gazetteergazetteerA 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. database, which is a separate download.

Next

  • Your first ten minutes — what works out of the box, what doesn't yet, and how to check your install.
  • What ships today — the per-localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for. picture and the known gaps.