Skip to main content

Hex Addressing

The core of AfriHex: every coordinate becomes a stable, human-readable code built on Uber's H3 hexagonal grid. The same coordinate always produces the same code — it's pure math, no database, no lookups. It even works offline.

Code format​

AF-GH-7-0GXTQD5RFZZZZ
│ │ │ └─ short ID (Crockford base-32 of the full H3 index)
│ │ └─ resolution (default 7 ≈ 5.2 km² per cell)
│ └─ country code (ISO 3166-1 alpha-2)
└─ AfriHex prefix

The short ID is a lossless encoding — the full H3 index is recovered from it.

The hex-code contract​

Everything a hex code guarantees, and everything it deliberately doesn't:

  • Deterministic. encode(lat, lng, resolution) is a pure function of its three inputs — the same coordinate at the same resolution always produces the same code, on any machine, offline, forever. There's no database, no server-side state, and nothing to go stale — see Design principles.
  • Resolution is stated, not implied. The 7 in AF-GH-7-0GXTQD5RFZZZZ is the resolution, and it's redundant with the resolution baked into the H3 index recovered from the short ID — the two can't disagree, because decoding derives one from the other. You never need side information to know what a code's precision is; it's in the string.
  • The format can evolve, but old codes don't get reinterpreted. AF is the scheme marker and GH the country profile — a future change to how AfriHex builds codes (a new alphabet, a new component) would ship under a new, distinguishable prefix rather than silently changing what AF-GH-<res>-… means. Decoding an existing code only requires recovering its H3 index and running H3's own (stable, versioned) grid math on it — nothing country-profile- or server-specific — so a code issued today keeps decoding to the exact same cell no matter what AfriHex ships later. See the note on ADDRESS_PROVIDER-independence below.
  • Not every coordinate — every coordinate inside the configured country. H3 tiles the entire globe, but /v2/hexcode checks the point against the country boundary first; outside it you get 400 OUT_OF_BOUNDS, not a code for the wrong country. Inside Ghana, encoding never fails for geographic reasons — there are no gaps, oceans, or exceptions.
  • A code identifies a cell — not a coordinate, a building, a verified address, or a service area. These five get conflated constantly, and the conflation is exactly how a hex code ends up misread as proof of residence or a precise entrance pin. See Five things that aren't the same below.

Five things that aren't the same​

ConceptWhat it actually isWhere it comes from
CoordinateA precise point — one (lat, lng) pair.A GPS fix, a geocoder result, or a field survey.
AfriHex codeA hierarchical geographic cell — an area, sized by resolution, not a point.Pure H3 math on a coordinate. Always exists for any point inside the country — no survey or evidence required.
BuildingA physical footprint or structure standing on the ground.External footprint data (e.g. Google Open Buildings) or a field survey — not derived from the hex grid itself.
Verified addressA claim, backed by evidence, that a specific address or location is genuine — not a fact about the world.POST /v2/kyc/verify's quality_score and optional signed certificate — see Identity & KYC.
Service areaA business-defined shape — a polygon, isochrone, or radius — represented internally as a set of hex cells for fast point-in-area checks.POST /v2/service-area — see Collections & Service Areas.

The clearest illustration of the first three is AfriHex's own precision hierarchy for "where do I actually put a pin". Decoding a hex code purely via H3 math (GET /v2/hexcode/{code}) always returns the cell centroid — a mathematical center, not evidence that a building, an entrance, or a person is actually there. GET /v2/address/resolve?hex=… (see Mobile Integration) layers real-world evidence on top, in decreasing order of trust:

Precision tierWhat it actually confirms
structureA field-surveyed door or gate for this specific location — the strongest tier.
building_edgeSnapped to a nearby building footprint — inferred from footprint data, not surveyed.
hex_centroidNo footprint or survey data exists — just the cell's mathematical center.

A hex_centroid result is the hex code doing exactly its job (identifying an area) with nothing else layered on top: no building, no survey, no verification, no address claim. Don't read it as any of those — and don't read a hex code alone as proof of residence, an exact entrance, or a verified address; that's what the structure precision tier and Identity & KYC's signed certificates are for, respectively.

Cell size by resolution​

Average cell area roughly halves-then-some with every step up in resolution:

ResolutionScaleAvg. area
4Region~1,770 km²
5District~253 km²
6Town~36.1 km²
7Suburb/neighbourhood (default)~5.16 km²
8Block~0.737 km²
9Small block~0.105 km²
10Street-level~0.015 km²

These are global H3 averages, not a fixed constant — individual cells vary in area (H3's grid isn't perfectly uniform across the globe), which is why the area_km2 in every response is computed for that specific cell rather than looked up from a table. Treat the numbers above as "what scale am I picking", and the response field as the source of truth for any one cell.

Behaviour near a cell boundary​

Cell membership is exact math with a hard edge — a coordinate is in exactly one cell at a given resolution, never "close to" one. The practical consequence is that ordinary GPS jitter (a few metres) can put two readings of the same physical spot into different, adjacent cells if that spot happens to sit near an edge. This is the same phenomenon the GhanaPostGPS bridge already surfaces explicitly via boundary_distance_m and near_grid_boundary in reverse-geocode responses — hex cells have the identical property, just without a dedicated flag today.

For anything decision-critical near an edge (geofence breaches, drift detection, proximity checks), don't rely on a single point-in-cell test at a fine resolution. Either use a coarser resolution for that check, or treat the cell's immediate neighbours (GET /v2/hexcode/{code}/neighbors, or disk?k=1) as part of the match.

Storing and displaying a code​

Store the full string (AF-GH-7-0GXTQD5RFZZZZ) — or the raw h3_index plus the resolution used — as an opaque, stable identifier, the way you'd store any primary key. Don't parse or reconstruct components client-side beyond display; decode through the API (or an H3 library) instead. As noted in Best practices, pick one resolution per use case and stick to it — codes at different resolutions aren't directly comparable (two codes are never "equal" or "adjacent" without first walking one to the other's resolution via /parent or /children).

Sharing and privacy​

Resolution is the privacy dial. A coarse code (4–6, region/district/town) is about as revealing as naming a city — safe to show in a public UI, share in a support ticket, or log without a second thought. A fine code (9–10, block/street level) is precise enough to identify roughly where a specific building sits, and should be handled with the same care as a raw GPS coordinate: access-controlled, and shared only with the same authorization you'd require for exact coordinates.

This matters most for the flagship use case: a borrower's home zone in Location Monitoring is captured under explicit customer consent and is meant to stay inside that consent-gated pipeline (drift detection, webhook alerts, the collections team) — not surfaced in a public-facing dashboard at a resolution fine enough to pinpoint a home. Pick a coarser resolution for anything user-facing that doesn't need street-level precision.

Encode coordinates → hex code​

GET /v2/hexcode?lat=5.6037&lng=-0.1870&res=7

curl "https://api.afrihex.com/v2/hexcode?lat=5.6037&lng=-0.1870&res=7" \
-H "X-API-Key: $AFRIHEX_API_KEY"
{
"success": true,
"data": {
"code": "AF-GH-7-0GXTQD5RFZZZZ",
"h3_index": "877576970ffffff",
"resolution": 7,
"country": "Ghana",
"country_code": "GH",
"center": { "lat": 5.604078493411575, "lng": -0.19529195005881006 },
"boundary": [
{ "lat": 5.609495946766828, "lng": -0.20575170258746575 },
{ "lat": 5.598573458939036, "lng": -0.20393981349281015 },
{ "lat": 5.5931560414662345, "lng": -0.19348099764158785 },
{ "lat": 5.598660521196899, "lng": -0.184831748179894 },
{ "lat": 5.609584560860423, "lng": -0.18664210146941776 },
{ "lat": 5.615002569057701, "lng": -0.1971032403752692 }
],
"area_km2": 3.86
}
}

Resolutions range from 4 (coarse regions) to 10 (street-level). The default is 7 — roughly "suburb" scale.

Decode hex code → coordinates​

GET /v2/hexcode/{code}

curl "https://api.afrihex.com/v2/hexcode/AF-GH-7-0GXTQD5RFZZZZ" \
-H "X-API-Key: $AFRIHEX_API_KEY"
{
"success": true,
"data": {
"code": "AF-GH-7-0GXTQD5RFZZZZ",
"h3_index": "877576970ffffff",
"resolution": 7,
"country": "Ghana",
"country_code": "GH",
"center": { "lat": 5.604078493411575, "lng": -0.19529195005881006 },
"area_km2": 3.86
}
}
EndpointReturns
GET /v2/hexcode/{code}/childrenThe 7 child cells (next finer resolution)
GET /v2/hexcode/{code}/neighborsThe 6 adjacent cells
GET /v2/hexcode/{code}/disk?k=2All cells within k hops (3k²+3k+1)
GET /v2/hexcode/{code}/parent?res=5The ancestor at a coarser resolution
GET /v2/hexcode/{code}/geojsonThe cell polygon as GeoJSON
# Zoom in: get the 7 finer cells inside a res-7 cell
curl "https://api.afrihex.com/v2/hexcode/AF-GH-7-0GXTQD5RFZZZZ/children" \
-H "X-API-Key: $AFRIHEX_API_KEY"
{
"success": true,
"data": {
"parent": "AF-GH-7-0GXTQD5RFZZZZ",
"children": [
{
"code": "AF-GH-8-0H1TQD5R1ZZZZ",
"h3_index": "8875769701fffff",
"resolution": 8,
"country_code": "GH",
"center": { "lat": 5.604078493411571, "lng": -0.19529195005881328 },
"area_km2": 0.55
}
]
}
}

neighbors and disk return the same cell objects under data.neighbors / data.cells; parent returns data.parent (a single cell object).

Distance and path​

GET /v2/hexcode/distance?origin=...&destination=... returns the number of grid hops between two cells. GET /v2/hexcode/path returns every cell along the shortest path.

Query parameters — both required:

ParameterTypeRequiredDefinition
originstringrequiredHex code of the starting cell
destinationstringrequiredHex code of the destination cell
curl "https://api.afrihex.com/v2/hexcode/distance?origin=AF-GH-7-0GXTQD5RFZZZZ&destination=AF-GH-7-0GXTQD5VFZZZZ" \
-H "X-API-Key: $AFRIHEX_API_KEY"
{
"success": true,
"data": {
"origin": "AF-GH-7-0GXTQD5RFZZZZ",
"destination": "AF-GH-7-0GXTQD5VFZZZZ",
"distance": 1
}
}

GET /v2/hexcode/path returns the same shape but with a cells array instead of distance — every cell along the shortest route.

Compact a region​

POST /v2/hexcode/compact replaces complete sets of child cells with their parent — useful for representing large areas efficiently.

Request object:

ParameterTypeRequiredDefinition
codesarray<string>requiredHex codes to compact
resolutionintegeroptionalResolution of the input cells (default 7)
curl -X POST "https://api.afrihex.com/v2/hexcode/compact" \
-H "X-API-Key: $AFRIHEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"codes": ["AF-GH-7-0GXTQD5RFZZZZ", "AF-GH-7-0GXTQD5VFZZZZ"], "resolution": 7}'
{
"success": true,
"data": {
"input_count": 2,
"output_count": 2,
"cells": [
{ "code": "AF-GH-7-0GXTQD5RFZZZZ", "h3_index": "877576970ffffff", "resolution": 7 },
{ "code": "AF-GH-7-0GXTQD5VFZZZZ", "h3_index": "877576976ffffff", "resolution": 7 }
]
}
}

Bulk encode​

POST /v2/hexcode/bulk converts up to 1,000 coordinates in one call.

Request object:

ParameterTypeRequiredDefinition
coordinatesarray<object>requiredUp to 1,000 { lat, lng } pairs
resolutionintegeroptionalH3 resolution (default 7, range 4–10)
curl -X POST "https://api.afrihex.com/v2/hexcode/bulk" \
-H "X-API-Key: $AFRIHEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"coordinates": [{"lat": 5.6037, "lng": -0.1870}, {"lat": 6.7, "lng": -1.62}], "resolution": 7}'
{
"success": true,
"data": {
"count": 2,
"results": [
{
"code": "AF-GH-7-0GXTQD5RFZZZZ",
"h3_index": "877576970ffffff",
"resolution": 7,
"country": "Ghana",
"country_code": "GH",
"center": { "lat": 5.604078493411575, "lng": -0.19529195005881006 },
"area_km2": 3.86
},
{
"code": "AF-GH-7-0GXTQD5WJ0000",
"h3_index": "87757697fffffff",
"resolution": 7,
"country": "Ghana",
"country_code": "GH",
"center": { "lat": 6.7, "lng": -1.62 },
"area_km2": 3.86
}
]
}
}

Map tiles​

GET /v2/map/tiles?bbox=...&res=7 returns a GeoJSON FeatureCollection of the cells covering a bounding box — render hex maps on Leaflet/Mapbox. Use the coarser res while zoomed out, then subdivide with /children as the user zooms in.

The bbox format is north,south,east,west (decimal degrees). Keep the area small enough that the tile stays under 5,000 cells — zoom in or raise res otherwise:

curl "https://api.afrihex.com/v2/map/tiles?bbox=5.62,5.60,-0.185,-0.195&res=8" \
-H "X-API-Key: $AFRIHEX_API_KEY"
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "area_km2": 0.55, "code": "AF-GH-8-0H1TQD5T3ZZZZ", "resolution": 8 },
"geometry": { "type": "Polygon", "coordinates": [] }
}
]
}

Bridge to GhanaPostGPS codes​

The bridge converts between GhanaPostGPS codes and hex codes:

# GPS code → hex code
curl "https://api.afrihex.com/v2/bridge/ghanapost/GA-142-7281" \
-H "X-API-Key: $AFRIHEX_API_KEY"
{
"success": true,
"data": {
"gps_code": "GA1427281",
"hex_code": "AF-GH-7-0GXTQD5EFZZZZ",
"coordinates": { "lat": 5.547396933004167, "lng": -0.207847593183683 },
"region": "Greater Accra",
"district": "Accra",
"quality_score": 1
}
}
note

The hex engine itself is upstream-independent. On deployments with ADDRESS_PROVIDER=none (or outside the addressed country), bridge calls fail with a clear error, but all encode/decode/navigation math keeps working.

Best practices​

  • Pick one resolution per use case and store it with the code (it's baked into the format).
  • Cache aggressively — encoding is pure math; there's never a reason to re-encode the same coordinate.
  • Use the SDK — afrihex.hex.encode / decode / children / neighbors / parent / distance / path / bulk.

API reference​

See Hex Addressing — API Reference for every endpoint in this group.