postcode

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 17 Imported by: 1

README

Nigerian Postcode Go SDK (postcode)

A high-performance, zero-allocation Go client library for Nigeria's National Digital Alphanumeric Postcode system (docs.postcode.gov.ng).

Go Reference Go Version

Features

  • Zero-Allocation Parsing: postcode.Parse() parses and validates 11-character postcodes with 0 heap escapes (~28ns/op).
  • Safe & Idiomatic Go: Fully memory-safe accessors, Go range-over-func iterators (postcode.ParseSeq), standard errors (errors.As), and functional options.
  • Protocol Conformance: Built to match the official NIPOST OpenAPI spec (GET /v1/lookup, GET /v1/search/autocomplete, GET /v1/search/nearby, GET /v1/search/reverse, and POST /v1/assembly/assemble).
  • Resilient Transport: Production-tuned HTTP transport with exponential backoff, full jitter, Retry-After header support, and safe connection draining.
  • Pluggable Observability: Zero external dependencies in the core module, with an optional OpenTelemetry module (github.com/abcubed3/postcode/otelpostcode).

Installation

Requires Go 1.27+:

go get github.com/abcubed3/postcode

Quickstart

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "time"

    "github.com/abcubed3/postcode"
)

func main() {
    ctx := context.Background()

    // 1. High-throughput client with functional options
    client, err := postcode.NewClient()
    if err != nil {
        log.Fatal(err)
    }

    // 2. Graded postcode lookup (Level 1 is free, Level 2-3 require commercial credits)
    res, err := client.Lookup(ctx, "EK 01 A03 FK 01", postcode.Level1)
    if err != nil {
        var apiErr *postcode.APIError
        if errors.As(err, &apiErr) && apiErr.IsInsufficientCredits() {
            log.Fatalf("Top up required: %v", apiErr)
        }
        log.Fatalf("Lookup failed: %v", err)
    }
    fmt.Printf("Postcode: %s, Valid: %t\n", res.Postcode, res.Valid)

    // 3. Segment-aware autocomplete
    suggs, err := client.Autocomplete(ctx, "EK 01 A")
    if err != nil {
        log.Fatalf("Autocomplete failed: %v", err)
    }
    fmt.Printf("Active segment: %s, Suggestions: %+v\n", suggs.Segment, suggs.Suggestions)

    // 4. Generate Google Maps URL & Coordinates from 11-digit postcode string
    gmapsURL, err := postcode.GoogleMapsURL("EK-01-A03-FK-01")
    if err != nil {
        log.Fatalf("Mapping failed: %v", err)
    }
    lat, lng, _ := postcode.Coordinates("EK-01-A03-FK-01")
    fmt.Printf("Google Maps URL: %s\n", gmapsURL)
    fmt.Printf("Coordinates: %.6f, %.6f\n", lat, lng)

    // 5. Batch streaming parse 
    records := []string{"EK 01 A03 FK 01", "INVALID_CODE", "FC 02 A09 DB 09"}
    for p, err := range postcode.ParseSeq(records) {
        if err != nil {
            fmt.Printf("Skipping invalid entry: %v\n", err)
            continue
        }
        fmt.Printf("Parsed: %s (State: %s, LGA: %s, Unit: %s)\n",
            p.Formatted(), p.State(), p.LGA(), p.BuildingUnit())
    }
}

API Surface

Method / Function Endpoint / Scope Description
postcode.GoogleMapsURL(code) Offline / Local Generates direct universal Google Maps URL for an 11-digit postcode.
postcode.Coordinates(code) Offline / Local Returns (latitude, longitude) for an 11-digit postcode string.
postcode.ResolveLocation(code) Offline / Local Resolves Location struct (lat/long, precision, Google Maps/Apple/OSM URLs).
p.GoogleMapsURL() Method on Postcode Zero-allocation / sub-microsecond Google Maps URL generation.
client.ResolveLocation(ctx, code) Gateway + Offline Resolves building location with Level 3 live enrichment and offline fallback.
client.Lookup(ctx, code, level) GET /v1/lookup Graded postcode lookup (Levels 1–3 cumulative per official spec).
client.Autocomplete(ctx, q) GET /v1/search/autocomplete Segment-aware suggestions for partial input.
client.Nearby(ctx, params) GET /v1/search/nearby Units within a radius (default 300m) of a coordinate.
client.Reverse(ctx, params) GET /v1/search/reverse Snaps coordinate to nearest active unit (default 25m).
client.Assemble(ctx, segs) POST /v1/assembly/assemble Assembles 5 segments into canonical format.
client.Disassemble(ctx, code) GET /v1/assembly/disassemble Decomposes code into its 5 administrative segments.

Command-Line Interface (postcode)

The repository includes a production-grade, Cobra- and Viper-powered CLI binary for terminal and pipeline workflows:

# Build and install the CLI
go install ./cmd/postcode
Key CLI Capabilities
  • Offline Validation: postcode validate "EK 01 A03 FK 01" (grammar, format, state codes, exit codes).
  • Segment Parsing: postcode parse "LA 11 W06 TC 10" -o json (extracts State, LGA, District, Area, Unit, Zone).
  • Format Normalization: cat dirty.txt | postcode format --style canonical (Unix pipe friendly).
  • Geocoding & Maps: postcode map EK-01-A03-FK-01 (generates Google Maps, Apple Maps, OSM URLs).
  • Batch CSV Processing: postcode batch --input orders.csv --output enriched.csv --column postcode (processes >350k rows/sec offline).
  • Gateway Operations: postcode lookup, postcode autocomplete, postcode nearby, postcode reverse, and postcode status.
  • Embedded Simulator: postcode serve --port 8080 (runs local mock NIPOST gateway directly).

See postcode --help or individual command help (postcode <cmd> --help) for full flag details.

Live Server Simulator

For local development and automated CI testing without incurring API fees or requiring live network access, the SDK includes an in-memory mock server pre-loaded with all official test postcodes from docs.postcode.gov.ng:

1. Run as a Standalone Server
# Start on localhost:8080 (or specify -port 8080)
go run ./cmd/simulator

Test with curl:

# L1 Public validity lookup (Free, no auth)
curl "http://localhost:8080/v1/lookup?code=EK-01-A03-FK-01&level=1"

# L3 Commercial lookup (Requires X-API-Key header)
curl "http://localhost:8080/v1/lookup?code=EK-01-A03-FK-01&level=3" \
  -H "X-API-Key: nipost_live_test"
2. Embed Directly in Go Unit & Integration Tests
import "github.com/abcubed3/postcode/simulator"

func TestMyService(t *testing.T) {
    srv := simulator.NewServer() // Starts httptest.Server
    defer srv.Close()

    client, err := postcode.NewClient(
        postcode.WithBaseURL(srv.URL),
        postcode.WithAPIKey("test_key"),
    )
    // Run your application tests against srv.URL
}
3. Preloaded Test Postcodes
Postcode State Locality / Address
EK-01-A03-FK-01 Ekiti NTA Road, Back of Fabian Hotel, Ado Ekiti
AK-11-I61-ZF-12 Akwa Ibom 12 Oron Road, Uyo
AK-11-H40-WD-11 Akwa Ibom 11 Wellington Bassey Way, Uyo
BA-02-M67-BL-69 Bauchi 69 Bank Road, GRA, Bauchi
BA-02-E99-NE-30 Bauchi 30 Ahmadu Bello Way, Bauchi
EB-13-G95-FR-90 Ebonyi 90 Ogoja Road, Abakaliki
EB-13-I97-AB-30 Ebonyi 30 Water Works Road, Abakaliki
EN-05-V19-CD-22 Enugu 22 Chime Avenue, New Haven, Enugu
EN-05-V19-FT-20 Enugu 20 Ogui Road, Enugu
FC-03-B06-AG-12 FCT 12 Shehu Shagari Way, Garki, Abuja
FC-02-B19-RT-30 FCT 30 Gado Nasko Way, Phase 4, Kubwa, Abuja
JI-24-O18-JP-23 Jigawa 23 Sani Abacha Way, Dutse
JI-24-N11-VM-58 Jigawa 58 Kano-Dutse Expressway, Dutse
KN-31-F82-WJ-80 Kano 80 Badu Road, Bompai, Kano
KN-31-D78-IQ-38 Kano 38 Ibrahim Taiwo Road, Kano
LA-11-W06-TC-10 Lagos 10 Obafemi Awolowo Way, Ikeja, Lagos
LA-11-U34-ZR-63 Lagos 63 Isaac John Street, GRA Ikeja, Lagos
NI-09-J67-QC-65 Niger 65 Bosso Road, Minna
NI-09-A75-DA-10 Niger 10 Paida Road, Minna
OG-14-T18-BN-16 Ogun 16 Lalubu Street, Oke-Ilewo, Abeokuta
OG-14-M82-QA-09 Ogun 9 Quarry Road, Abeokuta

Rate Limiting & Resilient Retries

As specified in the NIPOST Authentication Documentation, all authenticated gateway requests are rate-limited per key. The gateway returns quota headers on every response:

X-RateLimit-Limit: 600
X-RateLimit-Remaining: 597

When quota is exceeded, the gateway returns 429 Too Many Requests with a cooldown duration.

Intelligent 429 Handling
  • Automatic Cooldown Synchronization: On receiving HTTP 429, the transport extracts cooldown intervals from Retry-After (integer seconds or RFC1123 HTTP dates) and X-RateLimit-Reset, adding randomized jitter to avoid thundering herds when the window resets.
  • Fail-Fast Defense (MaxRateLimitDelay): If the gateway requests a cooldown longer than the configured threshold (default: 10s), the client aborts immediately, preventing goroutine hangs and wasted retries.
  • Live Quota Introspection: Call client.RateLimit() at any time to inspect Remaining, Limit, and ResetAt in a thread-safe manner without making extra network calls.
  • Rich Error Context: apiErr.IsRateLimit() returns true on 429 errors, exposing apiErr.RetryAfter and apiErr.RateLimit.
// 1. Fine-tune rate limit retries
client, err := postcode.NewClient(
    postcode.WithAPIKey("nipost_live_..."),
    postcode.WithRateLimitRetry(true, 5*time.Second), // Max wait threshold: 5s
)

// 2. Inspect rate limit quota
if rl := client.RateLimit(); rl != nil {
    fmt.Printf("Quota remaining: %d/%d (resets at %v)\n", rl.Remaining, rl.Limit, rl.ResetAt)
}

// 3. Structured error handling
res, err := client.Lookup(ctx, "EK-01-A03-FK-01", postcode.Level2)
if err != nil {
    var apiErr *postcode.APIError
    if errors.As(err, &apiErr) && apiErr.IsRateLimit() {
        log.Printf("Rate limit exceeded. Try again in %v", apiErr.RetryAfter)
    }
}

OpenTelemetry Integration

Install the telemetry adapter submodule:

go get github.com/abcubed3/postcode/otelpostcode

Hook it into your client:

package main

import (
    "context"
    "log"

    "github.com/abcubed3/postcode"
    "github.com/abcubed3/postcode/otelpostcode"
)

func main() {
    // Initialize tracing and metrics from global OTel providers
    tel, err := otelpostcode.NewTelemetry()
    if err != nil {
        log.Fatal(err)
    }

    client, err := postcode.NewClient(
        postcode.WithAPIKey("YOUR_API_KEY"),
        postcode.WithTelemetry(tel),
    )
    if err != nil {
        log.Fatal(err)
    }

    _, _ = client.Lookup(context.Background(), "EK 01 A03 FK 01", postcode.Level2)
}

Benchmarks

Microbenchmarks measured on darwin/arm64 (Apple M2 Max) using go test -bench=. -benchmem:

BenchmarkParse-12             42424563        28.25 ns/op          0 B/op        0 allocs/op
BenchmarkFormatted-12        195061753         6.15 ns/op          0 B/op        0 allocs/op
BenchmarkResolveLocation-12   10245876       115.70 ns/op         32 B/op        4 allocs/op
BenchmarkGoogleMapsURL-12      3721462       325.20 ns/op        128 B/op        7 allocs/op
  • postcode.Parse: Fully validates, normalizes, and extracts segments with 0 B/op and 0 allocs/op in ~28 ns/op.
  • postcode.Formatted: Formats canonical hyphenated postcodes (AA-99-H77-BB-55) with 0 B/op and 0 allocs/op in ~6 ns/op.
  • postcode.ResolveLocation: Resolves building and administrative coordinates offline in ~115 ns/op.
  • postcode.GoogleMapsURL: Formats and generates universal Google Maps search URLs in ~325 ns/op.

Documentation

Index

Constants

View Source
const (
	DefaultBaseURL = "https://api.postcode.gov.ng"
)

Variables

View Source
var (
	ErrInvalidLength = errors.New("postcode: invalid length, must be exactly 11 alphanumeric characters")
	ErrInvalidFormat = errors.New("postcode: invalid format structure")
)
View Source
var ErrUnknownState = errors.New("postcode: unknown nigerian state code")

ErrUnknownState indicates that a postcode has a state prefix not recognized in the Nigerian state registry.

View Source
var NigerianStates = map[string]StateRecord{
	"AB": {Code: "AB", Name: "Abia", Capital: "Umuahia", Latitude: 5.5320, Longitude: 7.4860, Zone: "SOUTH EAST"},
	"AD": {Code: "AD", Name: "Adamawa", Capital: "Yola", Latitude: 9.2094, Longitude: 12.4818, Zone: "NORTH EAST"},
	"AK": {Code: "AK", Name: "Akwa Ibom", Capital: "Uyo", Latitude: 5.0377, Longitude: 7.9128, Zone: "SOUTH SOUTH"},
	"AN": {Code: "AN", Name: "Anambra", Capital: "Awka", Latitude: 6.2209, Longitude: 7.0670, Zone: "SOUTH EAST"},
	"BA": {Code: "BA", Name: "Bauchi", Capital: "Bauchi", Latitude: 10.3158, Longitude: 9.8442, Zone: "NORTH EAST"},
	"BY": {Code: "BY", Name: "Bayelsa", Capital: "Yenagoa", Latitude: 4.9267, Longitude: 6.2676, Zone: "SOUTH SOUTH"},
	"BN": {Code: "BN", Name: "Benue", Capital: "Makurdi", Latitude: 7.7304, Longitude: 8.5211, Zone: "NORTH CENTRAL"},
	"BO": {Code: "BO", Name: "Borno", Capital: "Maiduguri", Latitude: 11.8333, Longitude: 13.1500, Zone: "NORTH EAST"},
	"CR": {Code: "CR", Name: "Cross River", Capital: "Calabar", Latitude: 4.9757, Longitude: 8.3417, Zone: "SOUTH SOUTH"},
	"DE": {Code: "DE", Name: "Delta", Capital: "Asaba", Latitude: 6.1984, Longitude: 6.7329, Zone: "SOUTH SOUTH"},
	"EB": {Code: "EB", Name: "Ebonyi", Capital: "Abakaliki", Latitude: 6.3249, Longitude: 8.1137, Zone: "SOUTH EAST"},
	"ED": {Code: "ED", Name: "Edo", Capital: "Benin City", Latitude: 6.3350, Longitude: 5.6037, Zone: "SOUTH SOUTH"},
	"EK": {Code: "EK", Name: "Ekiti", Capital: "Ado Ekiti", Latitude: 7.6211, Longitude: 5.2215, Zone: "SOUTH WEST"},
	"EN": {Code: "EN", Name: "Enugu", Capital: "Enugu", Latitude: 6.4584, Longitude: 7.5464, Zone: "SOUTH EAST"},
	"FC": {Code: "FC", Name: "Federal Capital Territory", Capital: "Abuja", Latitude: 9.0579, Longitude: 7.4951, Zone: "NORTH CENTRAL"},
	"GO": {Code: "GO", Name: "Gombe", Capital: "Gombe", Latitude: 10.2897, Longitude: 11.1711, Zone: "NORTH EAST"},
	"IM": {Code: "IM", Name: "Imo", Capital: "Owerri", Latitude: 5.4858, Longitude: 7.0355, Zone: "SOUTH EAST"},
	"JI": {Code: "JI", Name: "Jigawa", Capital: "Dutse", Latitude: 11.7594, Longitude: 9.3389, Zone: "NORTH WEST"},
	"KD": {Code: "KD", Name: "Kaduna", Capital: "Kaduna", Latitude: 10.5105, Longitude: 7.4165, Zone: "NORTH WEST"},
	"KN": {Code: "KN", Name: "Kano", Capital: "Kano", Latitude: 12.0022, Longitude: 8.5920, Zone: "NORTH WEST"},
	"KT": {Code: "KT", Name: "Katsina", Capital: "Katsina", Latitude: 12.9855, Longitude: 7.6171, Zone: "NORTH WEST"},
	"KE": {Code: "KE", Name: "Kebbi", Capital: "Birnin Kebbi", Latitude: 12.4504, Longitude: 4.1999, Zone: "NORTH WEST"},
	"KO": {Code: "KO", Name: "Kogi", Capital: "Lokoja", Latitude: 7.8023, Longitude: 6.7333, Zone: "NORTH CENTRAL"},
	"KW": {Code: "KW", Name: "Kwara", Capital: "Ilorin", Latitude: 8.4966, Longitude: 4.5421, Zone: "NORTH CENTRAL"},
	"LA": {Code: "LA", Name: "Lagos", Capital: "Ikeja", Latitude: 6.6018, Longitude: 3.3515, Zone: "SOUTH WEST"},
	"NA": {Code: "NA", Name: "Nasarawa", Capital: "Lafia", Latitude: 8.4932, Longitude: 8.5153, Zone: "NORTH CENTRAL"},
	"NI": {Code: "NI", Name: "Niger", Capital: "Minna", Latitude: 9.6139, Longitude: 6.5569, Zone: "NORTH CENTRAL"},
	"OG": {Code: "OG", Name: "Ogun", Capital: "Abeokuta", Latitude: 7.1475, Longitude: 3.3619, Zone: "SOUTH WEST"},
	"ON": {Code: "ON", Name: "Ondo", Capital: "Akure", Latitude: 7.2571, Longitude: 5.2058, Zone: "SOUTH WEST"},
	"OS": {Code: "OS", Name: "Osun", Capital: "Osogbo", Latitude: 7.7827, Longitude: 4.5418, Zone: "SOUTH WEST"},
	"OY": {Code: "OY", Name: "Oyo", Capital: "Ibadan", Latitude: 7.3775, Longitude: 3.9470, Zone: "SOUTH WEST"},
	"PL": {Code: "PL", Name: "Plateau", Capital: "Jos", Latitude: 9.9271, Longitude: 8.8921, Zone: "NORTH CENTRAL"},
	"RI": {Code: "RI", Name: "Rivers", Capital: "Port Harcourt", Latitude: 4.8156, Longitude: 7.0498, Zone: "SOUTH SOUTH"},
	"SO": {Code: "SO", Name: "Sokoto", Capital: "Sokoto", Latitude: 13.0609, Longitude: 5.2476, Zone: "NORTH WEST"},
	"TA": {Code: "TA", Name: "Taraba", Capital: "Jalingo", Latitude: 8.8937, Longitude: 11.3600, Zone: "NORTH EAST"},
	"YO": {Code: "YO", Name: "Yobe", Capital: "Damaturu", Latitude: 11.7470, Longitude: 11.9608, Zone: "NORTH EAST"},
	"ZA": {Code: "ZA", Name: "Zamfara", Capital: "Gusau", Latitude: 12.1702, Longitude: 6.6592, Zone: "NORTH WEST"},
}

NigerianStates contains geographic centroids and administrative capitals for all 36 States and FCT.

Functions

func BatchParseSeq

func BatchParseSeq(inputs []string) iter.Seq2[int, Result[Postcode]]

BatchParseSeq yields an iter.Seq2 iterator with slice index and Result wrapper.

func Coordinates added in v0.1.1

func Coordinates(raw string) (lat, lng float64, err error)

Coordinates parses any valid 11-character Nigerian postcode string and returns its latitude and longitude coordinates.

func GoogleMapsURL added in v0.1.1

func GoogleMapsURL(raw string) (string, error)

GoogleMapsURL parses any valid 11-character Nigerian postcode string and directly generates its Google Maps location URL.

func ParseSeq

func ParseSeq(inputs []string) iter.Seq2[Postcode, error]

ParseSeq streams parsed postcodes as (Postcode, error) pairs using Go iterators.

func RegisterKnownBuilding added in v0.1.1

func RegisterKnownBuilding(rec BuildingRecord)

RegisterKnownBuilding allows applications to register custom building units into the offline geocoding cache.

Types

type APIError

type APIError struct {
	StatusCode int            `json:"-"`
	Code       string         `json:"code"`
	Message    string         `json:"message"`
	RateLimit  *RateLimitInfo `json:"-"`
	RetryAfter time.Duration  `json:"-"`
}

APIError represents an error returned by the NIPOST Postcode API gateway.

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) IsInsufficientCredits

func (e *APIError) IsInsufficientCredits() bool

func (*APIError) IsNotFound

func (e *APIError) IsNotFound() bool

func (*APIError) IsRateLimit

func (e *APIError) IsRateLimit() bool

func (*APIError) IsUnauthorized

func (e *APIError) IsUnauthorized() bool

func (*APIError) UnmarshalJSON

func (e *APIError) UnmarshalJSON(data []byte) error

UnmarshalJSON handles both the official nested envelope {"error": {"code": ..., "message": ...}} and flat payloads {"code": ..., "message": ...}.

type AdministrativeAddress

type AdministrativeAddress struct {
	State        string `json:"state,omitempty"`
	StateName    string `json:"state_name,omitempty"`
	LGA          string `json:"lga,omitempty"`
	LGAName      string `json:"lga_name,omitempty"`
	District     string `json:"district,omitempty"`
	DistrictName string `json:"district_name,omitempty"`
	Area         string `json:"area,omitempty"`
	AreaName     string `json:"area_name,omitempty"`
	Unit         string `json:"unit,omitempty"`
	Zone         string `json:"zone,omitempty"`
	LocalityName string `json:"locality_name,omitempty"`
}

AdministrativeAddress details administrative boundaries up to area level (L2+).

type AssembledPostcode

type AssembledPostcode struct {
	Postcode string `json:"postcode"` // Canonical: "EK-01-A03-FK-01"
	Display  string `json:"display"`  // Spaced: "EK 01 A03 FK 01"
	Compact  string `json:"compact"`  // Compact: "EK01A03FK01"
}

AssembledPostcode represents the output of POST /v1/assembly/assemble.

type AutocompleteResponse

type AutocompleteResponse struct {
	Segment     string                   `json:"segment"`
	Suggestions []AutocompleteSuggestion `json:"suggestions"`
}

AutocompleteResponse contains suggestions for the currently active segment.

type AutocompleteSuggestion

type AutocompleteSuggestion struct {
	Code  string `json:"code"`
	Label string `json:"label"`
}

AutocompleteSuggestion represents an individual suggestion returned by autocomplete.

type BuildingRecord added in v0.1.1

type BuildingRecord struct {
	Postcode  string // Canonical (e.g. "EK-01-A03-FK-01") or Compact ("EK01A03FK01")
	Latitude  float64
	Longitude float64
	Address   string
	StateCode string
	StateName string
	LGACode   string
	LGAName   string
	Zone      string
}

BuildingRecord holds high-precision point geometry for a specific building unit.

type Client

type Client struct {
	// contains filtered or unexported fields
}

func NewClient

func NewClient(opts ...ClientOption) (*Client, error)

NewClient instantiates a production-tuned HTTP client.

func (*Client) Assemble

func (c *Client) Assemble(ctx context.Context, segs Segments) (*AssembledPostcode, error)

Assemble builds canonical, display, and compact postcodes from individual segments.

func (*Client) Autocomplete

func (c *Client) Autocomplete(ctx context.Context, query string) (*AutocompleteResponse, error)

Autocomplete retrieves segment-aware suggestions for a partial postcode input (e.g. "EK 01 A").

func (*Client) Disassemble

func (c *Client) Disassemble(ctx context.Context, code string) (*Segments, error)

Disassemble decomposes a postcode string into its 5 structural segments.

func (*Client) Lookup

func (c *Client) Lookup(ctx context.Context, code string, level LookupLevel) (*LookupResponse, error)

Lookup queries the gateway for detailed attributes of a postcode. Level defaults to Level1 (free) if 0 or unspecified.

func (*Client) Nearby

func (c *Client) Nearby(ctx context.Context, params NearbyParams) (*NearbyResponse, error)

Nearby searches for active postcode units within a radius (default 300m) of a coordinate.

func (*Client) RateLimit

func (c *Client) RateLimit() *RateLimitInfo

RateLimit returns the most recent rate limit quotas reported by the NIPOST gateway, or nil if no request has yet returned rate limit headers.

func (*Client) ResolveLocation added in v0.1.1

func (c *Client) ResolveLocation(ctx context.Context, code string) (*Location, error)

ResolveLocation resolves an 11-character Nigerian postcode to its geographic Location and Google Maps URL. It queries the NIPOST gateway at the official public Level 3 (or Level 2 for street & administrative address enrichment), checks for any custom point geometry, and leverages the SDK's high-precision offline reference geocoding engine to guarantee reliable coordinates and Google Maps URLs.

func (*Client) Reverse

func (c *Client) Reverse(ctx context.Context, params ReverseParams) (*ReverseResponse, error)

Reverse resolves a geographic coordinate to the nearest active postcode unit.

type ClientOption

type ClientOption func(*Client)

func WithAPIKey

func WithAPIKey(key string) ClientOption

WithAPIKey sets the NIPOST API key sent in the X-API-Key header. An apiKey is optional for public endpoints (L1 lookup, search, assembly).

func WithBaseTransport

func WithBaseTransport(rt http.RoundTripper) ClientOption

WithBaseTransport sets the underlying round tripper underneath the retry middleware.

func WithBaseURL

func WithBaseURL(customURL string) ClientOption

WithBaseURL overrides the default gateway URL (e.g. for testing with httptest.Server).

func WithRateLimitRetry

func WithRateLimitRetry(enabled bool, maxDelay time.Duration) ClientOption

WithRateLimitRetry configures automatic retry on 429 Too Many Requests responses. If enabled is false, 429 responses will not be retried. maxDelay sets the maximum duration the client is willing to sleep for a rate limit cooldown.

func WithRetryConfig

func WithRetryConfig(cfg RetryConfig) ClientOption

WithRetryConfig configures backoff, jitter, and retry limits.

func WithTelemetry

func WithTelemetry(t Telemetry) ClientOption

WithTelemetry registers custom observability hooks.

func WithTimeout

func WithTimeout(d time.Duration) ClientOption

WithTimeout sets the overall client request timeout.

func WithTransport

func WithTransport(rt http.RoundTripper) ClientOption

WithTransport explicitly overrides the entire round tripper stack (bypassing retries).

type LGARecord added in v0.1.1

type LGARecord struct {
	StateCode string
	LGACode   string
	Name      string
	Latitude  float64
	Longitude float64
}

LGARecord holds reference coordinates and names for an LGA within a state.

type Location added in v0.1.1

type Location struct {
	Postcode  string    `json:"postcode"`           // Canonical hyphenated: "EK-01-A03-FK-01"
	Compact   string    `json:"compact"`            // Compact 11-char: "EK01A03FK01"
	Latitude  float64   `json:"latitude"`           // Geographic latitude (-90 to +90)
	Longitude float64   `json:"longitude"`          // Geographic longitude (-180 to +180)
	Address   string    `json:"address,omitempty"`  // Known street/building address if available
	StateCode string    `json:"state_code"`         // 2-letter state code (e.g. "EK")
	StateName string    `json:"state_name"`         // Full state name (e.g. "Ekiti")
	LGACode   string    `json:"lga_code,omitempty"` // 2-digit LGA code (e.g. "01")
	LGAName   string    `json:"lga_name,omitempty"` // Full LGA name (e.g. "Ado Ekiti")
	Zone      string    `json:"zone,omitempty"`     // Geopolitical zone (e.g. "SOUTH WEST")
	Precision Precision `json:"precision"`          // Coordinate precision tier (placed at end to minimize struct alignment padding)
}

Location encapsulates geographic coordinates, address descriptors, and mapping URLs.

func ResolveLocation added in v0.1.1

func ResolveLocation(raw string) (Location, error)

ResolveLocation parses any valid 11-character Nigerian postcode string (compact, hyphenated, or spaced) and returns its resolved geographic Location. Uses local administrative reference data without requiring network calls.

func (Location) AppleMapsURL added in v0.1.1

func (l Location) AppleMapsURL() string

AppleMapsURL returns an Apple Maps URL for iOS/macOS devices. Format: https://maps.apple.com/?ll=lat,lng&q=...

func (Location) Coordinates added in v0.1.1

func (l Location) Coordinates() (lat, lng float64)

Coordinates returns the latitude and longitude as a float64 pair.

func (Location) GoogleMapsDirectionsURL added in v0.1.1

func (l Location) GoogleMapsDirectionsURL() string

GoogleMapsDirectionsURL returns a Google Maps navigation/directions URL to this location. Format: https://www.google.com/maps/dir/?api=1&destination=lat,lng

func (Location) GoogleMapsURL added in v0.1.1

func (l Location) GoogleMapsURL() string

GoogleMapsURL returns a standard, universal Google Maps search URL with exact coordinates. Format: https://www.google.com/maps/search/?api=1&query=lat,lng

func (Location) IsZero added in v0.1.1

func (l Location) IsZero() bool

IsZero reports whether the location represents a zero/uninitialized value.

func (Location) OpenStreetMapURL added in v0.1.1

func (l Location) OpenStreetMapURL() string

OpenStreetMapURL returns an OpenStreetMap pin URL.

func (Location) SearchQuery added in v0.1.1

func (l Location) SearchQuery() string

SearchQuery produces a descriptive location query string suitable for search engines (e.g. "Ado Ekiti, Ekiti State, Nigeria" or "Abuja Municipal, Federal Capital Territory, Nigeria").

type LookupLevel

type LookupLevel int

LookupLevel specifies the depth of attributes returned by a lookup request. Per official NIPOST documentation (docs.postcode.gov.ng/concepts/lookup-levels): Lookup responses are graded into 3 cumulative levels (L1–L3):

  • Level 1 (Free, Public): Validity status (valid: true/false) and canonical postcode.
  • Level 2 (Commercial): Administrative address (state, LGA, district, area) and street address.
  • Level 3 (Commercial): Full building use status (residential, commercial, mixed, etc.) and all lower levels.

Standard commercial API keys are strictly capped at Level 3 (LevelMax). Attempting to request a level higher than your organization's granted quota yields a 403 Forbidden ("key lacks scope or level").

Note on Level 4 & Level 5: In raw backend OpenAPI schemas (docs.postcode.gov.ng/api-reference/graded-postcode-lookup-levels-1–5-cumulative), levels 4 and 5 represent unreleased or restricted government tiers:

  • Level 4: other_building_info (unreleased building metadata)
  • Level 5: point_geometry (restricted government GIS coordinates)

Standard commercial keys do NOT have access to L4/L5. The SDK defines Level4 and Level5 for backward compatibility and private enterprise mock environments.

const (
	LevelDefault LookupLevel = 0 // Default to Level 1
	Level1       LookupLevel = 1 // Free: Validity check and canonical postcode
	Level2       LookupLevel = 2 // Commercial: Administrative boundaries and street address
	Level3       LookupLevel = 3 // Commercial: Building use status and unit metadata (Official Maximum)
	LevelMax     LookupLevel = 3 // Standard commercial maximum level (L3)

	// Restricted / Internal tiers (not accessible with standard API keys):
	Level4 LookupLevel = 4 // Internal/Enterprise: Additional building info
	Level5 LookupLevel = 5 // Restricted: High-precision point geometry (requires special government grant)
)

type LookupResponse

type LookupResponse struct {
	Postcode              string                 `json:"postcode"`
	Valid                 bool                   `json:"valid"`
	AdministrativeAddress *AdministrativeAddress `json:"administrative_address,omitempty"`
	RecentHouseAddress    *RecentHouseAddress    `json:"recent_house_address,omitempty"`
	BuildingUseStatus     string                 `json:"building_use_status,omitempty"`
	OtherBuildingInfo     map[string]any         `json:"other_building_info,omitempty"`
	PointGeometry         *PointGeometry         `json:"point_geometry,omitempty"`
}

LookupResponse represents the graded attributes returned by GET /v1/lookup.

type MetricsRecorder

type MetricsRecorder interface {
	RecordDuration(ctx context.Context, method, endpoint string, statusCode int, duration time.Duration)
	AddInflight(ctx context.Context, delta int64)
}

MetricsRecorder defines standard metrics contracts for client operations.

type NearbyParams

type NearbyParams struct {
	Latitude  float64
	Longitude float64
	RadiusM   float64 // Search radius in meters (default 300, max 300)
}

NearbyParams configures location search around a geographic coordinate.

type NearbyResponse

type NearbyResponse struct {
	Results []NearbyUnit `json:"results"`
}

NearbyResponse contains location units within the requested radius.

type NearbyUnit

type NearbyUnit struct {
	Postcode   string  `json:"postcode"`
	Display    string  `json:"display"`
	DistanceM  float64 `json:"distance_m"`
	Confidence string  `json:"confidence"`
	StateName  string  `json:"state_name,omitempty"`
	LGAName    string  `json:"lga_name,omitempty"`
	Address    string  `json:"address,omitempty"`
}

NearbyUnit represents a building unit found near a coordinate.

type PointGeometry

type PointGeometry struct {
	Type        string    `json:"type,omitempty"`
	Coordinates []float64 `json:"coordinates,omitempty"` // [lng, lat]
}

PointGeometry represents GeoJSON point coordinates (L5).

type Postcode

type Postcode struct {
	// contains filtered or unexported fields
}

Postcode represents an immutable, validated 11-byte Nigerian postcode. Backed by a fixed byte array to ensure zero heap escapes.

func Parse

func Parse(raw string) (Postcode, error)

Parse extracts and validates segments with 0 heap allocations.

func (Postcode) AppendText

func (p Postcode) AppendText(b []byte) ([]byte, error)

AppendText implements encoding.TextAppender for zero-alloc formatting. Outputs the spaced format: "AA 99 H77 BB 55".

func (Postcode) Area

func (p Postcode) Area() string

func (Postcode) BuildingUnit

func (p Postcode) BuildingUnit() string

func (Postcode) Coordinates added in v0.1.1

func (p Postcode) Coordinates() (lat, lng float64)

Coordinates returns the latitude and longitude coordinates for this postcode.

func (Postcode) District

func (p Postcode) District() string

func (Postcode) Formatted

func (p Postcode) Formatted() string

Formatted returns the canonical hyphenated representation (e.g. "EK-01-A03-FK-01").

func (Postcode) GoogleMapsURL added in v0.1.1

func (p Postcode) GoogleMapsURL() string

GoogleMapsURL returns a direct Google Maps search URL with exact coordinates.

func (Postcode) IsZero

func (p Postcode) IsZero() bool

func (Postcode) LGA

func (p Postcode) LGA() string

func (Postcode) Location added in v0.1.1

func (p Postcode) Location() Location

Location returns the resolved geographic location and mapping URLs for this postcode. Resolves offline via built-in administrative and building registries without network calls.

func (Postcode) MarshalText

func (p Postcode) MarshalText() ([]byte, error)

func (Postcode) Raw

func (p Postcode) Raw() string

Raw returns the compact 11-character representation (e.g. "EK01A03FK01").

func (Postcode) State

func (p Postcode) State() string

Safe segment accessors.

func (Postcode) String

func (p Postcode) String() string

String returns the spaced format (e.g. "EK 01 A03 FK 01").

func (*Postcode) UnmarshalText

func (p *Postcode) UnmarshalText(text []byte) error

type Precision added in v0.1.1

type Precision uint8

Precision indicates the resolution accuracy of the resolved coordinates.

const (
	// PrecisionBuilding indicates exact building-level point geometry.
	PrecisionBuilding Precision = iota
	// PrecisionArea indicates area or street axis level resolution.
	PrecisionArea
	// PrecisionDistrict indicates district-level centroid resolution.
	PrecisionDistrict
	// PrecisionLGA indicates Local Government Area centroid resolution.
	PrecisionLGA
	// PrecisionState indicates State-level centroid resolution.
	PrecisionState
)

func (Precision) MarshalJSON added in v0.1.1

func (p Precision) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler.

func (Precision) MarshalText added in v0.1.1

func (p Precision) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler.

func (Precision) String added in v0.1.1

func (p Precision) String() string

String returns the human-readable description of the precision level.

func (*Precision) UnmarshalJSON added in v0.1.1

func (p *Precision) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler to accept both string ("building") and numeric (0) forms.

func (*Precision) UnmarshalText added in v0.1.1

func (p *Precision) UnmarshalText(text []byte) error

UnmarshalText implements encoding.TextUnmarshaler.

type RateLimitInfo

type RateLimitInfo struct {
	Limit      int           // Maximum requests allowed in current window (from X-RateLimit-Limit)
	Remaining  int           // Remaining requests in current window (from X-RateLimit-Remaining)
	ResetAt    time.Time     // Timestamp when quota resets (from X-RateLimit-Reset or Retry-After)
	RetryAfter time.Duration // Cooldown duration requested by server (from Retry-After or X-RateLimit-Reset)
}

RateLimitInfo contains the rate-limiting quotas returned by the NIPOST API gateway as documented in https://docs.postcode.gov.ng/authentication.

func ParseRateLimitInfo

func ParseRateLimitInfo(h http.Header) *RateLimitInfo

ParseRateLimitInfo extracts rate limit quotas and cooldown durations from HTTP response headers.

type RecentHouseAddress

type RecentHouseAddress struct {
	Address      string `json:"address,omitempty"`
	Recent       string `json:"recent,omitempty"`
	HouseNumber  string `json:"house_number,omitempty"`
	StreetName   string `json:"street_name,omitempty"`
	LocalityName string `json:"locality_name,omitempty"`
}

RecentHouseAddress contains residential and street metadata (L2+).

type Result

type Result[T any] struct {
	Value T
	Err   error
}

type RetryConfig

type RetryConfig struct {
	MaxRetries        int           // Maximum retry attempts for transient errors (default: 3)
	BaseDelay         time.Duration // Initial exponential backoff delay (default: 100ms)
	MaxDelay          time.Duration // Maximum exponential backoff delay (default: 2s)
	DisableRateLimit  bool          // If true, 429 Too Many Requests will not be retried automatically
	MaxRateLimitDelay time.Duration // Maximum sleep duration client will wait for a 429 cooldown (default: 10s)
}

RetryConfig configures backoff, jitter, and rate-limit handling for HTTP requests.

type RetryTransport

type RetryTransport struct {
	Base   http.RoundTripper
	Config RetryConfig
}

func NewRetryTransport

func NewRetryTransport(base http.RoundTripper, cfg RetryConfig) *RetryTransport

func (*RetryTransport) RoundTrip

func (r *RetryTransport) RoundTrip(req *http.Request) (*http.Response, error)

type ReverseParams

type ReverseParams struct {
	Latitude     float64
	Longitude    float64
	MaxDistanceM float64 // Search radius in meters (default 25, max 250)
}

ReverseParams configures reverse geocoding to snap coordinates to the nearest unit.

type ReverseResponse

type ReverseResponse struct {
	Found      bool        `json:"found"`
	Coordinate []float64   `json:"coordinate"` // [lng, lat]
	Unit       *NearbyUnit `json:"unit,omitempty"`
	Area       string      `json:"area,omitempty"`
	District   string      `json:"district,omitempty"`
	State      string      `json:"state,omitempty"`
	Message    string      `json:"message,omitempty"`
	RadiusM    float64     `json:"radius_m"`
}

ReverseResponse represents the result of reverse geocoding a coordinate.

type Segments

type Segments struct {
	State    string `json:"state"`
	LGA      string `json:"lga"`
	District string `json:"district"`
	Area     string `json:"area"`
	Unit     string `json:"unit"`
}

Segments represents the 5 administrative components of a Nigerian postcode.

type Span

type Span interface {
	End()
	RecordError(err error)
	SetAttribute(key, value string)
	SetStatus(code int, msg string)
}

Span encapsulates active span operations without coupling to any trace vendor.

type StateRecord added in v0.1.1

type StateRecord struct {
	Code      string
	Name      string
	Capital   string
	Latitude  float64
	Longitude float64
	Zone      string
}

StateRecord holds reference coordinates and metadata for a Nigerian state.

type Telemetry

type Telemetry struct {
	Tracer  Tracer
	Metrics MetricsRecorder
}

Telemetry bundles tracing and metrics hooks.

func NopTelemetry

func NopTelemetry() Telemetry

NopTelemetry provides zero-overhead, no-op telemetry by default.

type Tracer

type Tracer interface {
	Start(ctx context.Context, operation string) (context.Context, Span)
}

Tracer defines the tracing contract for postcode operations.

Directories

Path Synopsis
cmd
simulator command
postcode module
otelpostcode module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL