Developer tools for Nigeria's National Digital Alphanumeric Postcode System (NDAPS), the building-level postcode NIPOST launched in October 2026: libraries for Rust and Python (ng-postcode), JavaScript (ng-postcode-js on npm), and Java (ng-postcode-api), MCP servers for AI assistants on PyPI and npm, and a resolver that turns described addresses into postcodes.
A postcode has 11 characters in five segments, written EK-01-A03-FK-01: state, LGA, district, area and building unit. NIPOST's own example, FC 02 A09 DB 09, is the same shape with spaces.
| Package | What it does | Install | Source |
|---|---|---|---|
ng-postcode (Rust) |
Parse, validate and format codes offline; client for the postcode.gov.ng API | cargo add ng-postcode |
src/, docs |
ng-postcode (Python) |
The same behaviour, with sync and async clients | pip install ng-postcode |
python/ |
ng-postcode-js |
The same behaviour for JavaScript and TypeScript, with a fetch client |
npm install ng-postcode-js |
js/packages/ng-postcode-js |
ng-postcode and ng-postcode-api (Java) |
The same behaviour for Java 17 and later; the core has no dependencies | io.github.adeniyikayodee:ng-postcode-api |
java/ |
ng-postcode-mcp |
MCP server: validate, look up, autocomplete, find by location, resolve addresses | uvx ng-postcode-mcp |
mcp/ |
ng-postcode-mcp (npm) |
The same server for Node, without address resolution | npx ng-postcode-mcp |
js/packages/ng-postcode-mcp |
ng-postcode-polars |
Polars expressions that parse, format and group codes by place, compiled from the Rust core | pip install ng-postcode-polars |
native/polars |
ng_postcode (PostgreSQL) |
The postcode as native 8-byte types with index support, compiled from the Rust core | Package from Releases, then CREATE EXTENSION ng_postcode |
native/pg |
ng_postcode (DuckDB) |
The same functions as a DuckDB extension, compiled from the Rust core | INSTALL ng_postcode FROM community (DuckDB 1.5.6) |
ng-postcode-duckdb |
ng-address-resolver |
Resolve free-text addresses to postcodes, only as precisely as the evidence allows (pre-alpha) | pip install ng-address-resolver |
agent/ |
Each package has its own README with full usage.
For databases, sql/ holds the postcode as a PostgreSQL domain and as DuckDB macros, in plain SQL that runs the same shared cases.
Two names are easy to get wrong. On npm the library is ng-postcode-js, because ng-postcode there is another project. ng-address-resolver is imported as ng_address.
For online stores, ng-postcode-woocommerce is a WooCommerce plugin built on the same rules and the same shared test cases: it checks and tidies the postcode at checkout, finds it from the customer's location, and makes shipping zones match on postcode prefixes.
For React and Next.js, templates/nigerian-address-form is a checkout address form built with shadcn/ui and ng-postcode-js. Add it with npx shadcn@latest add https://adeniyikayodee.github.io/ng-postcode/r/nigerian-address-form.json, or deploy the folder as an app.
Parsing and validation are offline and need no key.
Python
from ng_postcode import Postcode, parse
match parse("ek 01 a03 fk 01"):
case Postcode() as code:
print(code, code.compact) # EK-01-A03-FK-01 EK01A03FK01
case error:
print(error) # e.g. "invalid lga segment"JavaScript and TypeScript
import { Postcode, parse } from "ng-postcode-js";
const code = parse("ek 01 a03 fk 01");
if (code instanceof Postcode) {
console.log(String(code), code.compact); // EK-01-A03-FK-01 EK01A03FK01
} else {
console.log(String(code)); // e.g. "invalid lga segment"
}Java
import io.github.adeniyikayodee.ngpostcode.Postcode;
if (Postcode.parse("ek 01 a03 fk 01") instanceof Postcode code) {
System.out.println(code + " " + code.compact()); // EK-01-A03-FK-01 EK01A03FK01
}Rust
use ng_postcode::{Postcode, Segment};
let code: Postcode = "ek 01 a03 fk 01".parse()?;
assert_eq!(code.to_string(), "EK-01-A03-FK-01");
assert_eq!(code.prefix(Segment::Area), "EK-01-A03-FK");AI assistants
The MCP server works with any MCP client. It runs over stdio as uvx ng-postcode-mcp, with the API key in the environment. Most clients take this entry in their MCP settings:
{
"mcpServers": {
"ng-postcode": {
"command": "uvx",
"args": ["ng-postcode-mcp"],
"env": { "NG_POSTCODE_API_KEY": "nipost_live_..." }
}
}
}To run it with Node, use "command": "npx" and "args": ["-y", "ng-postcode-mcp"]; that edition has every tool except address resolution. Per-client steps for Cursor, VS Code, Codex, Claude and others are in the MCP server README. Validation works without a key; the other tools need one from the NIPOST developer dashboard. The server is listed in the MCP Registry as io.github.Adeniyikayodee/ng-postcode.
PostgreSQL
Load the plain SQL file with psql -f sql/postgres.sql. It needs no extension, so it runs on managed services.
CREATE TABLE customers (
id bigint PRIMARY KEY,
postcode ng_postcode -- refuses anything that is not a compact code
);
INSERT INTO customers VALUES (1, ng_postcode_parse('ek 01 a03 fk 01'));
SELECT ng_postcode_format(ng_postcode_truncate(postcode, 'district')) AS district, count(*)
FROM customers
GROUP BY 1;
-- EK-01-A03 | 1On a server where you can install an extension, the native extension has the same functions and stores a code in 8 bytes. Unpack the package for your PostgreSQL version from Releases, then run CREATE EXTENSION ng_postcode.
DuckDB
Load the same functions with duckdb my.db < sql/duckdb.sql.
SELECT ng_postcode_parse('ek 01 a03 fk 01');
-- EK01A03FK01The compiled extension, ng-postcode-duckdb, has the same functions: on DuckDB 1.5.6, run INSTALL ng_postcode FROM community, then LOAD ng_postcode.
Polars
import polars as pl
import ng_postcode_polars # pip install ng-postcode-polars
frame = pl.DataFrame({"postcode": ["ek 01 a03 fk 01", "EK-01-A03-ZZ-14", "100001"]})
print(frame.with_columns(code=pl.col("postcode").ng.parse(), ok=pl.col("postcode").ng.is_valid()))
# ek 01 a03 fk 01 EK01A03FK01 true
# EK-01-A03-ZZ-14 EK01A03ZZ14 true
# 100001 null falseA CSV file
pip install ng-postcode
ng-postcode --column postcode customers.csv --out checked.csvThe output keeps every row and adds the compact code, a status of valid, suggested, invalid, or empty, a suggested correction, and the reason for each invalid row.
Language models trained before October 2026 expect a six-digit Nigerian postcode. To point a coding agent at the current format, paste this into your project's AGENTS.md, CLAUDE.md, or editor rules:
Nigerian postcodes: since 1 October 2026 a building's postcode is NIPOST's 11-character
digital postcode (NDAPS), such as EK-01-A03-FK-01, and a six-digit pattern does not match it.
Validate and format it with ng-postcode (pip, cargo), ng-postcode-js (npm), or
io.github.adeniyikayodee:ng-postcode (Maven). On npm, ng-postcode is a different project.
Reference: https://adeniyikayodee.github.io/ng-postcode/llms.txt
| Segment | Example | Shape |
|---|---|---|
| State | EK |
2 letters |
| LGA | 01 |
2 digits, 01 to 99 |
| District | A03 |
3 letters or digits |
| Area | FK |
2 letters |
| Building unit | 01 |
2 digits, 01 to 99 |
| Form | Example | Use it to |
|---|---|---|
| Canonical | EK-01-A03-FK-01 |
Write a code and pass it between systems |
| Spaced | EK 01 A03 FK 01 |
Show a code to people |
| Compact | EK01A03FK01 |
Store and compare codes |
Input may be hyphenated, spaced or compact, in either case. The compact form matches ^[A-Z]{2}(0[1-9]|[1-9][0-9])[A-Z0-9]{3}[A-Z]{2}(0[1-9]|[1-9][0-9])$. A well-formed code is not necessarily assigned to a building; only the NIPOST API can confirm that.
docs/schemas/postcode-reference.schema.json defines a small JSON object for handing a location from one system or AI agent to another: the code, its level (state to building), and optionally a confidence and whether NIPOST confirmed it. A resolved or partial resolve_address answer already fits it.
For a whole address, docs/address-record.md wraps the reference in an address record and maps it onto ISO 20022, FHIR, schema.org, vCard, and UPU's address layout for Nigeria.
- One behaviour, four languages: Rust, Python, JavaScript and Java all run the cases in
spec/, so they cannot drift apart. The Node MCP server registers its tools fromspec/mcp.json, which the Python server generates. - Offline first: parsing and validation never touch the network. API access is a separate, optional layer.
- Errors are values: expected failures, such as a malformed code or a rejected API key, come back as return values.
- Careful with money and guesses: the MCP server caps lookups at the free level unless told otherwise, and never corrects a mistyped code into a paid call. The resolver gives an area or district code when that is all the evidence supports.
- The NIPOST API needs a key for every endpoint, from the developer dashboard. Offline validation needs nothing.
- The API layer is tested against responses captured from the live API with a level 1 key, kept in
spec/responses.json. Runscripts/live_check.pywith your own key to repeat the comparison. Lookup levels 2 and up need a higher-access key and are tested only against NIPOST's documented examples. - The resolver and the
resolve_addresstool are pre-release. They work against the live API, but their accuracy on real addresses is unmeasured. Described addresses need a geocoder you run or pay for; text alone rarely identifies a building, so ask users for a location pin when the exact building matters. - Notes for integrators on how the live API behaves, each with the date it was observed, are in
docs/api-differences.md.
See CONTRIBUTING.md for the shared spec, how to add a language, and the commit standards.
cargo test --all-features # Rust
cd python && uv run --group dev pytest # Python library
cd mcp && uv run --group dev pytest # MCP server
cd agent && uv run --group dev pytest # resolver
cd js && npm ci && npm test # JavaScript library and Node MCP server
cd java && ./mvnw verify # Java librariesCI runs formatting, linting, type checks and tests for every package. Releases publish from tags (v* to crates.io; py-v*, mcp-v* and agent-v* to PyPI and the MCP Registry; js-v* and js-mcp-v* to npm) through trusted publishing, so no tokens are stored. Maven Central has no trusted publishing, so java-v* uses a token and a signing key held as environment secrets, and the upload is published by hand. An mcp-v* release also creates a GitHub release, which Glama rebuilds its listing from, and republishes the Smithery bundle with a key held the same way.
MIT