Skip to main content
Glama
README.md
# ng-postcode

[![CI](https://github.com/Adeniyikayodee/ng-postcode/actions/workflows/ci.yml/badge.svg)](https://github.com/Adeniyikayodee/ng-postcode/actions/workflows/ci.yml)
[![crates.io](https://img.shields.io/crates/v/ng-postcode.svg)](https://crates.io/crates/ng-postcode)
[![PyPI](https://img.shields.io/pypi/v/ng-postcode.svg?label=pypi%20ng-postcode)](https://pypi.org/project/ng-postcode/)
[![MCP server](https://img.shields.io/pypi/v/ng-postcode-mcp.svg?label=pypi%20ng-postcode-mcp)](https://pypi.org/project/ng-postcode-mcp/)
[![npm](https://img.shields.io/npm/v/ng-postcode-js.svg?label=npm%20ng-postcode-js)](https://www.npmjs.com/package/ng-postcode-js)
[![npm MCP server](https://img.shields.io/npm/v/ng-postcode-mcp.svg?label=npm%20ng-postcode-mcp)](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

A3.9/5.0

Scored across 5 tools

Disambiguation4/5

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.

Naming Consistency4/5

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.

Tool Count5/5

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.

Completeness4/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues