@princemercado/postcode-nl-validator
v0.1.0
Published
Validate and look up Dutch postcodes and addresses (offline, SQLite-backed).
Downloads
133
Maintainers
Readme
@princemercado/postcode-nl-validator
Offline validator and lookup for Dutch postcodes + addresses, backed by a local SQLite database built from the Nederlandse-adressen-en-postcodes dataset.
Ships as:
- A TypeScript/Node library (
createValidator,PostcodeValidatorclass). - A CLI for building the database and running a lightweight HTTP microservice.
How it works
- You run
postcode-nl buildonce on the server. It stream-decompressesNederland.csv.zst(~72 MB → ~1.4 GB), parses all ~9.9 M addresses, and writes a SQLite file (~700 MB) with indexes on(postcode, huisnummer). - At runtime, the library opens the SQLite file read-only — lookups are O(log n) index seeks (~0.05 ms each) and memory stays near-flat (SQLite page cache only).
Install
cd postcode-nl-validator
npm install
npm run buildIf installing better-sqlite3 fails on Windows, make sure you have build tools:
npm install --global --production windows-build-tools…or install a prebuilt binary:
npm install better-sqlite3 --build-from-source=falseBuild the database (one-off, ~3–6 minutes)
From the repo root (so the relative path resolves):
node postcode-nl-validator/bin/postcode-nl.js build \
--input Nederland.csv.zst \
--output postcode-nl-validator/postcodes.dbYou'll see a progress line like [build] 1,234,567 rows | 37.8% of source read. The final file is ~700 MB. Store it wherever you like on the server and point the library at it.
Library usage
import { createValidator } from '@princemercado/postcode-nl-validator';
const validator = createValidator({ dbPath: '/var/data/postcodes.db' });
// or: export POSTCODE_NL_DB=/var/data/postcodes.db and omit the option
// 1) Validate + get matches in one call
const result = validator.validate('1011 ab', 99);
// {
// valid: true,
// normalizedPostcode: '1011AB',
// reason: 'ok',
// matches: [{ straat: 'De Ruijterkade', huisnummer: 99, ... }]
// }
// 2) Just fetch addresses
const addresses = validator.lookup('1012NZ', 1);
// [{ huisletter: 'A', ... }, { huisletter: 'B', ... }]
// 3) Exact match on full identifier
const addr = validator.findExact('2511CS', 42, { huisnummertoevoeging: '2' });
// Cheap synchronous format check (no DB access)
import { isValidPostcodeFormat } from '@princemercado/postcode-nl-validator';
isValidPostcodeFormat('1011AB'); // trueReturn shape
interface Address {
straat: string;
huisnummer: number;
huisletter: string | null;
huisnummertoevoeging: string | null;
postcode: string; // always canonical '1234AB'
woonplaats: string;
gemeente: string;
provincie: string;
lat: number; // WGS84
lon: number;
}
interface ValidationResult {
valid: boolean;
normalizedPostcode: string | null;
matches: Address[];
reason: 'ok' | 'invalid_format' | 'invalid_huisnummer'
| 'postcode_not_found' | 'huisnummer_not_found';
}HTTP microservice
node bin/postcode-nl.js serve --db ./postcodes.db --port 3000Endpoints:
| Method | Path | Response |
|--------|-------------------------------------------------------|----------|
| GET | /health | { status, rows } |
| GET | /validate?postcode=1011AB&huisnummer=99 | ValidationResult |
| POST | /validate (JSON body with postcode, huisnummer) | ValidationResult |
| GET | /lookup?postcode=1011AB&huisnummer=99 | { matches: Address[] } |
Example:
curl 'http://localhost:3000/validate?postcode=1011AB&huisnummer=99'Running behind a reverse proxy (nginx/Caddy) and a process manager (systemd/pm2) is recommended. The server is single-process, but better-sqlite3 readers are lock-free on the same file, so you can run multiple workers sharing one postcodes.db.
One-shot CLI check
node bin/postcode-nl.js check --db ./postcodes.db --postcode 1011AB --huisnummer 99Exit code 0 = valid, 2 = not valid, 1 = bad arguments.
Testing
npm testThe test suite seeds a small in-memory SQLite fixture — it does not require the full 700 MB database.
Updating the data
When Nederland.csv.zst is refreshed upstream, just rerun postcode-nl build. It truncates the adressen table and rebuilds indexes. Keep the old DB around until the new build finishes and swap atomically with mv.
Why SQLite instead of in-memory?
| Option | RAM | Startup | Lookup | DB size |
|--------|-----|---------|--------|---------|
| Load all rows into a Map | ~2–4 GB | 30–60 s | ~0.001 ms | — |
| SQLite (this package) | ~20 MB | <50 ms | ~0.05 ms | ~700 MB |
| Ship a tiny postcode Set | ~20 MB | <50 ms | ~0.001 ms | ~2 MB (no address data) |
SQLite is the only option that gives you full address lookup without keeping a gigabyte of JS objects in memory on every worker.
