Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ it, and makes only free calls unless you pass `--paid`.
- Never commit a key, and never put one in an issue or a pull request.
- Level 2 and above return a person's address. Do not commit captured
responses that contain one.
- When the live API differs from its documentation, record it in the README
under "Where the live API differs from its docs", with the date observed,
and capture the body in `spec/responses.json` if it holds no personal data.
- When the live API differs from its documentation, record it in
`docs/api-differences.md`, with the date observed, and capture the body in
`spec/responses.json` if it holds no personal data.

## Branches and pull requests

Expand Down
10 changes: 5 additions & 5 deletions CRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ Part of the [ng-postcode project](https://github.com/Adeniyikayodee/ng-postcode)

An 11-character code in five segments: state, LGA, district, area, building unit.

| Style | Example |
| --- | --- |
| Canonical | `EK-01-A03-FK-01` |
| Display | `EK 01 A03 FK 01` |
| Compact | `EK01A03FK01` |
| 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 |

Compact form as a regular expression: `^[A-Z]{2}(0[1-9]|[1-9][0-9])[A-Z0-9]{3}[A-Z]{2}(0[1-9]|[1-9][0-9])$`

Expand Down
76 changes: 38 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ Developer tools for Nigeria's National Digital Alphanumeric Postcode System (NDA

A postcode has 11 characters in five segments, written `EK-01-A03-FK-01`: state, LGA, district, area and building unit.

## Start here

| To | Use |
| --- | --- |
| Check, tidy, or format a code someone typed | The library for your language. It works offline, with no key. |
| Confirm a code is assigned to a building | The same library's API client, with a key from the [NIPOST developer dashboard](https://dashboard.postcode.gov.ng) |
| Give an AI assistant postcode tools | `ng-postcode-mcp` |
| Find the postcode for a described address or a location pin | `ng-address-resolver`, or the `resolve_address` tool of the Python MCP server (both pre-release) |
| Take postcodes at a WooCommerce checkout | [ng-postcode-woocommerce](https://github.com/Adeniyikayodee/ng-postcode-woocommerce) |

## Packages

| Package | What it does | Install | Source |
Expand All @@ -25,27 +35,13 @@ A postcode has 11 characters in five segments, written `EK-01-A03-FK-01`: state,

Each package has its own README with full usage.

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.

## Quick start

**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 server is listed in the MCP Registry as `io.github.Adeniyikayodee/ng-postcode`.
Parsing and validation are offline and need no key.

**Python**

Expand Down Expand Up @@ -92,6 +88,24 @@ 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`.

## 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:
Expand All @@ -114,6 +128,12 @@ Reference: https://adeniyikayodee.github.io/ng-postcode/llms.txt
| 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
Expand All @@ -132,27 +152,7 @@ Input may be hyphenated, spaced or compact, in either case. The compact form mat
- 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.

### Where the live API differs from its docs

Observed on 3 October 2026:

- Every endpoint needs a key, including search, assembly and level 1 lookup, which the docs describe as public.
- Lookup also returns `status` (`valid`, `not_found`, `invalid`) and `verified`. A malformed code is answered with HTTP 200 and `status: invalid`.
- Autocomplete suggestions carry only `code`, the value of the next segment. The documented `label` is not sent.
- Reverse geocoding also returns `depth`.
- Nearby search, which the docs leave unspecified, returns a list of `postcode`, `display` and `distance_m`, nearest first.
- Asking for a level the key lacks returns `403 level_not_granted`.
- An empty autocomplete query never gets a response, so the libraries refuse to send one.
- `EK-01-A03-FK-01`, the example used throughout NIPOST's docs, is reported as not assigned.

Observed on 6 October 2026, with a level 1 test key:

- An empty autocomplete query is answered, with no suggestions. The libraries still refuse to send one.
- `FC-03-B06-AG-12`, autocomplete, reverse geocoding and nearby search all return empty answers where [`spec/responses.json`](spec/responses.json) records data. `scripts/live_check.py` lists each difference.
- A lookup level the API cannot read, or `0`, is answered at level 1.
- A latitude outside -90 to 90 returns `500 internal`.
- A request the load balancer rejects, such as a 5,000-character autocomplete query, returns `403` with an HTML body.
- Where the live API differs from NIPOST's documentation is recorded, with the date of each observation, in [`docs/api-differences.md`](docs/api-differences.md).

## Development

Expand Down
18 changes: 18 additions & 0 deletions docs/api-differences.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Where the live API differs from its docs

What `api.postcode.gov.ng` does where [NIPOST's documentation](https://docs.postcode.gov.ng) says otherwise or says nothing. Each row gives the date it was last observed. The 6 October observations were made with a level 1 test key. To repeat the comparison with your own key, run [`scripts/live_check.py`](https://github.com/Adeniyikayodee/ng-postcode/blob/main/scripts/live_check.py).

| Topic | The live API | Observed |
| --- | --- | --- |
| Keys | Every endpoint needs a key, including search, assembly and level 1 lookup, which the docs describe as public. | 3 October 2026 |
| Lookup | Also returns `status` (`valid`, `not_found`, `invalid`) and `verified`. A malformed code is answered with HTTP 200 and `status: invalid`. | 3 October 2026 |
| Lookup level | Asking for a level the key lacks returns `403 level_not_granted`. | 3 October 2026 |
| Lookup level | A level the API cannot read, or `0`, is answered at level 1. | 6 October 2026 |
| Autocomplete | Suggestions carry only `code`, the value of the next segment. The documented `label` is not sent. | 3 October 2026 |
| Autocomplete | An empty query is answered with no suggestions. On 3 October it never got a response, so the libraries refuse to send one. | 6 October 2026 |
| Reverse geocoding | Also returns `depth`. | 3 October 2026 |
| Reverse geocoding | A latitude outside -90 to 90 returns `500 internal`. | 6 October 2026 |
| Nearby search | Returns a list of `postcode`, `display` and `distance_m`, nearest first. The docs leave it unspecified. | 3 October 2026 |
| Load balancer | A request it rejects, such as a 5,000-character autocomplete query, returns `403` with an HTML body. | 6 October 2026 |
| Example code | `EK-01-A03-FK-01`, the example used throughout NIPOST's docs, is reported as not assigned. | 3 October 2026 |
| Test keys | With a level 1 test key, `FC-03-B06-AG-12`, autocomplete, reverse geocoding and nearby search all return empty answers where [`spec/responses.json`](https://github.com/Adeniyikayodee/ng-postcode/blob/main/spec/responses.json) records data. | 6 October 2026 |
12 changes: 9 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,17 @@ A well-formed code is not necessarily assigned to a building. Only the NIPOST AP

```python
from ng_postcode import Postcode
from ng_postcode.api import lookup
from ng_postcode.client import Client # pip install "ng-postcode[client]"
from ng_postcode.api import ApiError, lookup
from ng_postcode.client import Client, TransportError # pip install "ng-postcode[client]"

with Client(api_key="nipost_live_...") as client:
found = client.send(lookup(Postcode("FC03B06AG12"), level=1))

match found:
case ApiError() | TransportError():
print(found) # e.g. "invalid_api_key (401): ..."
case _:
print(found.valid, found.status) # True valid
```

```rust
Expand All @@ -73,7 +79,7 @@ let client = Client::new(std::env::var("NG_POSTCODE_API_KEY")?);
let found = client.send(&api::lookup("FC-03-B06-AG-12".parse()?, 1)?)?;
```

The API layer also covers autocomplete, reverse geocoding and nearby search.
Level 1 confirms a code is assigned. Levels 2 and up return address details and consume credits. The API layer also covers autocomplete, reverse geocoding and nearby search.

## Use it from an AI assistant

Expand Down
Loading
Loading