ng-postcode
# ng-postcode
[](https://github.com/Adeniyikayodee/ng-postcode/actions/workflows/ci.yml)
[](https://crates.io/crates/ng-postcode)
[](https://pypi.org/project/ng-postcode/)
[](https://pypi.org/project/ng-postcode-mcp/)
[](https://www.npmjs.com/package/ng-postcode-js)
[](https://www.npmjs.com/package/ng-postcode-mcp)
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.
## Packages
| 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/`](src), [docs](https://docs.rs/ng-postcode) |
| `ng-postcode` (Python) | The same behaviour, with sync and async clients | `pip install ng-postcode` | [`python/`](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`](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/`](java) |
| `ng-postcode-mcp` | MCP server: validate, look up, autocomplete, find by location, resolve addresses | `uvx ng-postcode-mcp` | [`mcp/`](mcp) |
| `ng-postcode-mcp` (npm) | The same server for Node, without address resolution | `npx ng-postcode-mcp` | [`js/packages/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`](native/polars) |
| `ng_postcode` (PostgreSQL) | The postcode as native 8-byte types with index support, compiled from the Rust core | Package from [Releases](https://github.com/Adeniyikayodee/ng-postcode/releases), then `CREATE EXTENSION ng_postcode` | [`native/pg`](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](https://github.com/Adeniyikayodee/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/`](agent) |
Each package has its own README with full usage.
For databases, [`sql/`](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](https://github.com/Adeniyikayodee/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`](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.
## Quick start
Parsing and validation are offline and need no key.
**Python**
```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**
```ts
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**
```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**
```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:
```json
{
"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](mcp#install). Validation works without a key; the other tools need one from the [NIPOST developer dashboard](https://dashboard.postcode.gov.ng). 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.
```sql
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 | 1
```
On a server where you can install an extension, the [native extension](native/pg) has the same functions and stores a code in 8 bytes. Unpack the package for your PostgreSQL version from [Releases](https://github.com/Adeniyikayodee/ng-postcode/releases), then run `CREATE EXTENSION ng_postcode`.
**DuckDB**
Load the same functions with `duckdb my.db < sql/duckdb.sql`.
```sql
SELECT ng_postcode_parse('ek 01 a03 fk 01');
-- EK01A03FK01
```
The compiled extension, [ng-postcode-duckdb](https://github.com/Adeniyikayodee/ng-postcode-duckdb), has the same functions: on DuckDB 1.5.6, run `INSTALL ng_postcode FROM community`, then `LOAD ng_postcode`.
**Polars**
```python
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 false
```
**A CSV file**
```sh
pip install ng-postcode
ng-postcode --column postcode customers.csv --out checked.csv
```
The 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.
## For coding agents
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:
```text
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
```
## The format
| 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.
## Passing a postcode between systems
[`docs/schemas/postcode-reference.schema.json`](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`](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.
## Design
- **One behaviour, four languages:** Rust, Python, JavaScript and Java all run the cases in [`spec/`](spec), so they cannot drift apart. The Node MCP server registers its tools from [`spec/mcp.json`](spec/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.
## Status
- The NIPOST API needs a key for every endpoint, from the [developer dashboard](https://dashboard.postcode.gov.ng). 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`](spec/responses.json). Run [`scripts/live_check.py`](scripts/live_check.py) with 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_address` tool 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`](docs/api-differences.md).
## Development
See [CONTRIBUTING.md](CONTRIBUTING.md) for the shared spec, how to add a language, and the commit standards.
```sh
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 libraries
```
CI 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.
## License
MIT
TDQS
Scored across 5 tools
Each tool targets a distinct step (validate structure, confirm assignment, autocomplete, reverse-geocode, forward-resolve), and descriptions explicitly cross-reference which to use when. Some boundary blur remains because validate_postcode, lookup_postcode, and resolve_address all return canonical postcode info, but the guidance resolves most confusion.
All names are snake_case verb_noun (validate_postcode, lookup_postcode, autocomplete_postcode, find_postcode_at_location, resolve_address). find_postcode_at_location breaks the tight noun-only pattern by appending a locative clause, but the convention is still clearly readable and consistent.
Five tools is well-scoped for a focused Nigerian postcode service, with one tool per core operation and no redundant or filler entries. Each earns its place.
The surface covers the full lifecycle: structure validation, assignment lookup, autocomplete, reverse geocoding, and forward address resolution, with graceful degradation for paid-level gating. Minor gaps exist (e.g. no batch validation), but they are easy to work around.