What you'd build with it
Recognizable jobs you've lost a week to, done without the assembly tax — and every one runs on open data you can audit.
Parse
Untangle any address line
Paste a gnarly, half-formatted, mistyped address and watch the neural parser label every component — house number, street, unit, city, postcode — across locales, live in the page.
"Apt 4, 12 Rue de Rivoli, 75001 Paris" → unit·house·street·postcode·cityGeocode
Geocode on-device
Resolve an address to a real coordinate with no server, no API key, and no query leaving the machine. A 37.6 MB model — one download, a few seconds on ordinary broadband, cached after that — and a byte-ranged global gazetteer do it in the tab.
"350 5th Ave, New York" → 40.7484, -73.9857 · rooftopMatch
Match records by place, not spelling
Resolve fragmented records with no shared key into entities. Block on the geocoded place, then match on canonicalized names, with a calibrated score and an abstain band for review.
123 Main St + 123 Main Street Apt 2 → 1 entityWorked examples, on real public data
Coverage reconciliation
The provider registry meets the Universal Service Fund
Three public datasets — a national provider registry, an FCC funding file, a state licensing list — that share no identifier. Resolved onto one map by matching the geocoded place, not the key none of them carry. Every dot is a real entity that turned up in more than one of them.
Data provenance
We keep the receipt on every coordinate
Every point Mailwoman resolves to remembers which open dataset it came from. Here's New York: the federal National Address Database statewide, OpenAddresses (the city's own data) in New York City. Most geocoders sand that provenance off. We keep it on the point.
How it compares
Good tools sit in every column here — several of them taught us the trade. The difference is what you stand up before the first query, and what each answer carries with it.
| Mailwoman | Nominatim | Photon | Pelias | Hosted APIs | |
|---|---|---|---|---|---|
| Before your first query | npm install | PostgreSQL + planet import | Nominatim import + OpenSearch | Elasticsearch cluster + imports | create an account |
| API key | none | none | none | none | required |
| Monthly fee | none | none (self-host) | none (self-host) | none (self-host) | metered |
| Runs in the browser, offline | ✅ | — | — | — | — |
| Calibrated confidence per component | ✅ | — | — | heuristic | varies |
Already running one of these? Mailwoman ships drop-in Nominatim-, Photon-, and libpostal-compatible APIs, so your client code can stay put. The full capability matrix — including the rows where the other tools win — is in How Mailwoman compares, with switching guides for each.
Neural address parser
ONNX-runtime sequence classifier over a SentencePiece tokenizer. Emits BIO-labeled components (country / region / locality / postcode / street / venue / …). Trained on a corpus stitched from TIGER, NAD, BAN, OpenAddresses + curated rows. Ships per-locale weight bundles as separate npm packages.
WOF-backed resolver
Parsed components are resolved to Who's On First place IDs + WGS-84 coordinates via FTS5 + R*Tree over pre-indexed SQLite shards. Pure node:sqlite, no SpatiaLite, no native build deps. Multi-shard ATTACH routes postcode queries to the postalcode shard automatically.
Pure-TypeScript runtime
Mailwoman runs on Node 22+ and, as the live demo shows, entirely in the browser — the classifier on onnxruntime-web, the resolver on sql.js-httpvfs over a byte-ranged gazetteer. The same pipeline client-side, no API server.
Spatial tools for agents
A poi_query kind and a sealed poi.db resolve category searches ("coffee near Honolulu") the way address queries resolve. @mailwoman/mcp exposes the same parse / geocode / POI-search toolset to any MCP-compatible agent over stdio. On npm since 7.2.1.
Quick start
Library
npm install mailwoman @mailwoman/neural @mailwoman/neural-weights-en-us
import { createRuntimePipeline } from "mailwoman"
import { NeuralAddressClassifier } from "@mailwoman/neural"
import { createWOFResolver, type ResolverBackend } from "@mailwoman/resolver"
import { WOFSqlitePlaceLookup } from "@mailwoman/resolver-wof-sqlite"
const classifier = await NeuralAddressClassifier.loadFromWeights({ locale: "en-US" })
const lookup = new WOFSqlitePlaceLookup({ databasePath: "./wof.sqlite" })
const parse = createRuntimePipeline({ classifier, resolver: createWOFResolver(lookup as unknown as ResolverBackend) })
const result = await parse("1600 Pennsylvania Ave NW, Washington DC")
// → house_number "1600" · street "Pennsylvania Ave NW" · locality "Washington" · region "DC"
// …and with the resolver attached, each resolved node carries coordinates + its source:
// locality "Washington" → lat 38.90, lon -77.04 · src resolver:locality
CLI
# parse + resolve in one shot
MAILWOMAN_WOF_DB=/path/to/wof.db npx mailwoman parse \
--neural --resolve --format xml \
"Springfield, Illinois"
<address raw="Springfield, Illinois">
<region src="resolver:region:85688697" lat="40.27" lon="-89.19">Illinois
<locality src="resolver:locality:85940429"
lat="39.80" lon="-89.65"
place="wof:85940429">Springfield</locality>
</region>
</address>

