Documentation
kenya-regions
Every way Kenya is divided up: country, counties, constituencies, wards, the former provinces, and the code schemes that disagree with each other. All of it bundled offline, with a typed API.
Getting started
Install
npm install kenya-regionsNo runtime dependencies and no network access. Every figure ships inside the package, so it works the same in Node, a browser bundle, a build step, an edge function or a serverless cold start.
The package is published as both ESM and CommonJS with full type declarations for each, verified against a real install on every commit.
Getting started
Quick start
import { kenya, counties, getCounty, getWardsByCounty, search } from 'kenya-regions'
kenya.codes.iso3166Alpha2 // 'KE'
counties.length // 47
getCounty(47) // Nairobi, by constitutional code
getCounty('KE-30') // Nairobi, by ISO 3166-2 code
getCounty('nairobi') // Nairobi, by slug
getWardsByCounty('Kiambu').length // 60
search('mbita')[0].region.name // 'Suba North', found by its former nameCommonJS works identically:
const { counties, getCounty } = require('kenya-regions')Concepts
Kenya is divided up in more than one way
This is the reason the package exists. “Kenya’s regions” is not one list. Several schemes are in active use, they were created for different purposes, and the part that causes bugs is that they do not nest into each other.
Counties
47 · devolvedCreated by the 2010 Constitution. Each has a governor, a senator and a county assembly. The primary modern unit.
Constituencies
290 · electoralEach elects one member of the National Assembly. Fixed at 290 by the Constitution itself.
Wards
1450 · electoralCounty assembly wards, each electing one MCA. The finest unit in the electoral hierarchy.
Administrative sub-counties
301 · national gov’tRun by Deputy County Commissioners. A second hierarchy that shares a name with the constituency but not its boundaries.
Former provinces
8 · historicalAbolished in 2013, still everywhere in older data and everyday speech. Partitions the country cleanly.
ISO 3166-2:KE
47 · internationalThe same counties, numbered alphabetically. Different numbers for the same places.
OCHA p-codes
humanitarianUsed by HDX, ReliefWeb and IPC. These do follow the constitutional numbering.
Economic blocs
7 · voluntaryCounty groupings under Article 189(2). They overlap, and they do not cover the whole country.
ASAL classification
23 · functionalArid and semi-arid lands, used for drought and food-security programming. Not administrative.
Concepts
The one hierarchy that nests
Counties, constituencies and wards form a genuine tree, and their codes carry it: each level is numbered sequentially within its parent.
Stable through 2027
The Constitution pins constituencies at 290, and in January 2026 the IEBC deferred the next boundary review until after the 2027 general election. The 47 / 290 / 1450 counts hold through that cycle.
Because the seats follow the map, the parliamentary arithmetic falls straight out of these counts: 290 constituencies elect 290 MPs, 47 counties elect 47 senators and 47 woman representatives, and 1450 wards elect 1450 MCAs.
Concepts
“Sub-county” means two different things
This is the most common source of confusion in Kenyan administrative data, and datasets rarely say which sense they mean. Both are shipped here, under separate names, so you never have to guess which one you are holding.
| Sense | The unit | Count | Export | Headed by |
|---|---|---|---|---|
| County government | The constituency | 290 | constituencies | Elected MP |
| National government | Administrative sub-county | 301* | subCounties | Deputy County Commissioner |
Section 48 of the County Governments Act 2012 makes a county’s decentralised units equivalent to the constituencies within it, which is why nearly every Kenyan address form labels the constituency “sub-county”. The national government’s sub-counties are a separate set with different boundaries.
They overlap heavily but not completely: 248 of the 301 share a name with a constituency, and 53 do not. Baringo shows the divergence plainly. Six of each, and not the same six:
getSubCountiesByCounty('Baringo').map((s) => s.name)
// Baringo Central, Baringo North, Koibatek, Marigat, Mogotio, Tiaty
getConstituenciesByCounty('Baringo').map((k) => k.name)
// Baringo Central, Baringo North, Baringo South, Eldama Ravine, Mogotio, TiatyBuilding an address form?
You almost certainly want constituencies. That is what “sub-county” means on nearly every Kenyan form, and it is the fully-coded, fully-validated hierarchy.
* How many sub-counties are there?
Nobody publishes a definitive machine-readable answer, and the number keeps moving. 301 is what ships here, from the KNBS listing, and it matches the AfroCave table, which also totals 301. Wikipedia says 314 as of 2023. In November 2024 the Interior Ministry gazetted 27 more, alongside 59 divisions, 170 locations and 322 sub-locations across 31 counties, which puts the current figure at about 341.
So the shipped 301 is behind. The gap is not closed because no authoritative register of the current set is published, and press lists of the 27 are unreliable, several printing 31 names under a headline count of 27. Guessing would mix invented units in with sourced ones, with nothing to tell them apart. What each source says is recorded in data/sources/subcounty-counts.json.
Why sub-counties have no numbers
Counties, constituencies and wards all carry official codes. Administrative sub-counties do not, so this package keys them by slug instead. Inventing a numbering would imply an authority the package does not have, and would be indistinguishable from the official codes elsewhere in the data. Slugs are unique across all 301, so they work as a primary key on their own.
Coverage
The source lists sub-counties against ward names rather than codes, so wards are matched by name within their county. 1,438 of the 1,450 wards map to a sub-county, or 99.2%. The remaining 12 have subCounty: null rather than a guess, and are listed in data/sources/name-conflicts.json. getWardsByCounty is always complete.
The national administration continues below this level: sub-county → division → location → sub-location, ending at the Assistant Chief. KNBS census enumeration uses that chain, which is why census microdata will not join cleanly to a ward-level table.
Concepts
Districts, divisions, locations, sub-locations
Below the sub-county the national administration keeps going, down to the Assistant Chief. The full chain, as enumerated by the 2009 census:
import { districts } from 'kenya-regions/districts'
import { subLocations } from 'kenya-regions/sublocations'
subLocations[0].population[2009] // census population
subLocations[0].households
subLocations[0].areaKm2
subLocations[0].densityPerKm2Sub-locations are the only level below county carrying population, household and area figures, because they are what the census counts. Their populations sum to exactly 38,610,097, the published 2009 national total, and the build asserts this, independently confirming all 7,150 rows.
A 2009 snapshot, not the current register
Districts no longer exist as administrative units, and 59 divisions, 170 locations and 322 sub-locations were gazetted in November 2024 alone. Use this for joining census-era data, for the location names chiefs still work with, and for historical analysis. It is not a description of Kenya today.
Codes at these four levels are assigned by this package, derived deterministically from the sorted hierarchy so they stay stable across builds. Unlike county, constituency and ward codes they carry no official authority, and names repeat across parents, so a name alone is never a key.
All four are subpath-only and are not re-exported from kenya-regions. Sub-locations alone outweigh everything else in the package, so nobody pays for them unless they ask.
Concepts
Two numbering systems, same 47 counties
The single most likely silent bug when joining Kenyan datasets. ISO numbers the counties alphabetically. The Constitution numbers them geographically. They are never the same number.
| County | Constitutional code | ISO 3166-2 | OCHA p-code |
|---|---|---|---|
| Mombasa | 1 | KE-28 | KE001 |
| Baringo | 30 | KE-01 | KE030 |
| Nairobi | 47 | KE-30 | KE047 |
getCounty(30).name // 'Baringo'
getCounty('KE-30').name // 'Nairobi' ← a different county entirelyBoth live on every county record as code and isoCode. isoToCounty() refuses a bare number, so the two cannot be mixed up by accident.
OCHA p-codes are the friendly ones: they follow the constitutional numbering exactly, so KE047 is county 47, and constituency p-codes append the constituency code, giving KE047275.
Before 2014, ISO coded the provinces
ISO 3166-2:KE listed the eight provinces (KE-110 Nairobi, KE-200 Central, …) until the 2014 update deleted them and added the counties. Those old codes are preserved on each province as legacyIsoCode for reading historical data.
Concepts
County explorer
Every identifier the package holds for a single county, side by side. Sort the list by each numbering scheme to see how far apart they run.
Concepts
The map
The counties, drawn from the outlines the package ships. Hover to name one; click to open it in the atlas, where its constituencies, sub-counties and wards are laid out, along with the districts and divisions that occupied the same ground before 2013.
The same geometry ships three ways: as GeoJSON from kenya-regions/outlines, as a styleable data/svg/counties.svg, and as an svg_path column in data/csv/county-outlines.csv for anyone drawing a map without a mapping library.
API reference
Data exports
Every dataset is exported directly as a frozen array, in code order. Import it and use it.
| Export | Type | Records | Order |
|---|---|---|---|
kenya | Country | 1 | |
counties | readonly County[] | 47 | Constitutional code |
countiesByName | readonly County[] | 47 | Alphabetical |
constituencies | readonly Constituency[] | 290 | IEBC code |
wards | readonly Ward[] | 1450 | IEBC code |
subCounties | readonly SubCounty[] | 301 | County, then name |
provinces | readonly Province[] | 8 | Conventional |
blocs | readonly EconomicBloc[] | 7 | Membership size |
Raw JSON is published too, for anyone who wants the data without the API:
import counties from 'kenya-regions/data/counties.json' with { type: 'json' }API reference
Lookups
Every lookup accepts a Query, which is number | string and covers each way a region is normally identified. Matching ignores case, spacing, apostrophes, slashes and hyphens, so 'Elgeyo/Marakwet', 'elgeyo-marakwet' and 'ELGEYO MARAKWET' all resolve.
| Accepted by | Forms |
|---|---|
getCounty | code, padded code, ISO code, p-code, name, slug, former name |
getConstituency | code, padded code, p-code, name, slug, former name |
getWard | code, padded code, name, slug, spelling variant |
getCounty(query: Query): County | undefined
Resolves a county by any of its identifiers. Returns undefined rather than guessing.
getCounty(47) // by constitutional code
getCounty('047') // zero-padded works too
getCounty('KE-30') // ISO 3166-2
getCounty('KE047') // OCHA p-code
getCounty('Nairobi City') // an alias
getCounty('Kampala') // undefinedgetConstituency(query: Query): Constituency | undefined
Resolves a constituency, including by names retired in the 2013 delimitation.
getConstituency('Mbita') // → Suba North
getConstituency('Gwasi') // → Suba South
getConstituency('Dujis') // → Garissa Township
getConstituency('Kilimani') // → Dagoretti NorthgetWard(query: Query): Ward | undefined
Resolves a ward. Ward names are not unique nationally. Several counties have a “Township” or a “Central”, so a name query returns the lowest-coded match. Use the code, or scope with getWardsByConstituency, when precision matters.
findWardsByName(name: string): Ward[]
Every ward with the given name, across all counties. The honest answer when a ward name is ambiguous.
getProvince(query: string): Province | undefined
getBloc(query: string): EconomicBloc | undefined
Provinces resolve by short code ('RFT'), name, or legacy ISO code ('KE-700'). Blocs resolve by short code, full name, or alternative name.
requireCounty · requireConstituency · requireWard
Identical to the get* pair but throw a descriptive error instead of returning undefined. Use these when a missing region is a bug rather than a possibility.
requireCounty('Kampala') // throws: Unknown county: "Kampala"API reference
Overlay schemes
getCountiesByProvince(province: ProvinceCode | string): County[]
Provinces partition the country: every county belongs to exactly one.
getCountiesByProvince('Rift Valley').length // 14
getCountiesByProvince('RFT').length // 14, the code works toogetCountiesByBloc(bloc: BlocCode | string): County[]
Blocs are the opposite of a partition.
Blocs overlap and do not cover Kenya
Lamu and Tana River sit in both FCDC and JKP. Nandi and Trans Nzoia sit in both LREB and NOREB. Narok sits in none of them. Treat bloc membership as a many-to-many tag on a county, never as a parent region, which is why county.economicBlocs is an array.
| Code | Bloc | Counties |
|---|---|---|
LREB | Lake Region Economic Bloc | 14 |
MKAREB | Mt. Kenya and Aberdares Region Economic Bloc | 10 |
NOREB | North Rift Economic Bloc | 8 |
FCDC | Frontier Counties Development Council | 7 |
JKP | Jumuiya ya Kaunti za Pwani | 6 |
NAMETRO | Nairobi Metropolitan Area | 5 |
SEKEB | South Eastern Kenya Economic Bloc | 3 |
getAsalCounties(kind?: 'arid' | 'semi-arid'): County[]
The 23 Arid and Semi-Arid Lands counties: 9 arid and 14 semi-arid, together covering over 80% of Kenya’s land area.
getAsalCounties() // 23
getAsalCounties('arid') // 9
getAsalCounties('semi-arid') // 14This is a county-level simplification of something that is really sub-county-level: Kieni in Nyeri and Mbeere in Embu are ASAL areas inside counties that otherwise are not.
getCitiesCounties(): County[]
Counties whose headquarters holds city status under the Urban Areas and Cities Act 2011: Nairobi, Mombasa, Kisumu, Nakuru (2021) and Uasin Gishu for Eldoret (2024). The year is on county.cityStatusSince.
API reference
Search
search(query: string, init?: SearchInit): SearchResult[]
Alias-aware fuzzy search across all three levels, built for type-ahead inputs. Results are ranked and labelled with the level that matched.
search('kis', { levels: ['county'], limit: 5 })
search('mbita') // → Suba North, matched: 'Mbita'
search('nairobi', { levels: ['ward'] })| Option | Type | Default |
|---|---|---|
levels | Level[] | all three |
limit | number | 10 |
threshold | number (0–1) | 0.4 |
Each result carries level, the region record, a score from 0 to 1, and matched, the actual string that matched, which may be an alias rather than the current name. Showing matched in the UI explains to the user why a result appeared.
Ranking is tiered rather than purely fuzzy: exact match, then prefix, then substring, then whole-token prefix, then edit distance. Counties break ties upward, because “Kisumu” is a county, a constituency and a ward, and the county is almost always the intended answer.
API reference
Select options
The most common reason to install this package is a dropdown, so the shape is provided directly.
countyOptions(init?) · constituencyOptions(init?) · wardOptions(init?)
Each returns { label, value, region }[]. The region is the full record, so you never have to look it back up after a selection.
countyOptions()
// [{ label: 'Baringo', value: '30', region: {…} }, …] alphabetical
countyOptions({ alphabetical: false, valueKey: 'slug' })
constituencyOptions({ county: 47 })
wardOptions({ constituency: 'Westlands' })| Option | Applies to | Default |
|---|---|---|
alphabetical | all | true |
valueKey | all | 'code' |
county | constituency, ward | |
constituency | ward |
valueKey accepts 'code', 'slug' or 'name', plus 'isoCode' and 'pcode' for counties, which helps when the value has to match what a backend already stores.
API reference
Code conversion
countyToIso(query): string | undefined
isoToCounty(isoCode: string): County | undefined
countyToPcode(query): string | undefined
fromPcode(pcode: string): County | Constituency | undefined
countyToIso(47) // 'KE-30'
isoToCounty('KE-30') // Nairobi
countyToPcode(47) // 'KE047'
fromPcode('KE047') // Nairobi county
fromPcode('KE047275') // Dagoretti North constituencyisoToCounty is strict on purpose
It accepts 'KE-30' and '30' as an ISO code, but never silently treats a bare number as a constitutional code. Passing 30 meaning “Baringo” returns Nairobi’s ISO entry or nothing. It never returns a quiet wrong answer.
slugify(input: string): string
The same normaliser used to build every slug in the datasets, exported so your own identifiers can match.
API reference
The country record
Above every subdivision sits kenya: the identifiers the rest of the world uses, and the national figures the subdivisions roll up to.
import { kenya } from 'kenya-regions'
kenya.codes.iso3166Alpha2 // 'KE' ISO 3166-1
kenya.codes.iso3166Alpha3 // 'KEN'
kenya.codes.unM49 // '404' UN statistical
kenya.codes.callingCode // '+254' ITU
kenya.currency.code // 'KES' ISO 4217
kenya.timeZone.iana // 'Africa/Nairobi'
kenya.location.borders // ETH SOM SSD TZA UGA
kenya.location.boundingBox // { west, south, east, north }The codes nest into the subdivision data, and the build asserts it: every county isoCode extends codes.iso3166Alpha2, and every county pcode extends codes.ochaPcode. The country record is the root of the same code trees the counties sit in.
Where Kenya sits in UN M49
001 World → 002 Africa → 202 Sub-Saharan Africa → 014 Eastern Africa → 404 KenyaDerived, never typed in
kenya.subdivisions and kenya.legislature are computed from the actual datasets at build time, so they cannot drift out of step with them.
| Chamber | Seats | Composition |
|---|---|---|
| National Assembly | 349 | 290 constituency + 47 county woman rep + 12 nominated |
| Senate | 67 | 47 elected + 16 women + 2 youth + 2 PWD |
| County assemblies | 1450 | one elected member per ward |
Speakers sit ex officio and are excluded from the totals.
Helpers
toInternationalPhone(input: string): string | undefined
formatCurrency(amount: number, options?): string
isPostalCode(input: string): boolean
toInternationalPhone('0712 345 678') // '+254712345678'
toInternationalPhone('12345') // undefined
formatCurrency(1234.5) // 'KSh 1,234.50'
isPostalCode('00100') // trueformatCurrency asks Intl for the currency code and substitutes the symbol itself, because ICU renders KES as “Ksh”, “KSh” or “KES” depending on the Node version and browser. Pass currencyDisplay to take the runtime’s own output instead.
API reference
Geometry
Coarse county outlines ship with this package, at about 15 KB gzipped. Enough to draw a national map and to answer which county a point falls in.
import { countyOutlines, locateCounty } from 'kenya-regions/outlines'
map.addSource('counties', { type: 'geojson', data: countyOutlines })
locateCounty(-1.2864, 36.8172)?.properties.name // 'Nairobi'
locateCounty(51.5, -0.12) // undefined, not in KenyaArguments are latitude then longitude, matching how coordinates are spoken and written, even though GeoJSON stores them the other way round.
Every county record also carries a bbox, so a map can be fitted to a county without loading any geometry at all:
getCounty('Nairobi').bbox // [36.663, -1.442, 37.104, -1.158]countyOutlines: CountyOutlineCollection
locateCounty(lat: number, lng: number): CountyOutline | undefined
getCountyOutline(code: number): CountyOutline | undefined
outlineContains(outline: CountyOutline, lat: number, lng: number): boolean
Feature properties carry code, name, slug and pcode, so getCounty(feature.properties.code) gives you the full record.
Accuracy is bounded by the simplification
Boundaries are simplified to roughly a kilometre, so a point close to a county border can resolve to the wrong side of it. Anything that has to hold at the edge needs a finer tier.
Finer tiers and other levels
Not published. Boundaries at higher detail, and levels below the county, are parked in kenya-regions-geo, which is not on npm. It waits for a use case the outlines here do not already cover.
The gap worth having is constituency and ward boundaries. That is blocked on finding a source whose geometry can be trusted rather than on effort: the obvious one has its Nairobi polygons carrying the administrative sub-counties rather than the constituencies, so a point in the CBD resolves to the wrong constituency. See the plan.
API reference
Types
interface County {
code: number // 1–47, constitutional
name: string
slug: string
capital: string
isoCode: string // 'KE-01'–'KE-47', alphabetical, not code
pcode: string // 'KE001'–'KE047', OCHA
formerProvince: string
formerProvinceCode: ProvinceCode
economicBlocs: BlocCode[] // may be empty, may hold several
asal: 'arid' | 'semi-arid' | null
cityStatusSince: number | null
areaKm2: number
population: { 2009: number; 2019: number }
centroid: { lat: number; lng: number } | null
bbox: [number, number, number, number] | null // west, south, east, north
aliases: string[]
}
interface Constituency {
code: number // 1–290
name: string
slug: string
countyCode: number
pcode: string // e.g. 'KE047275'
areaKm2: number | null
centroid: { lat: number; lng: number } | null
aliases: string[]
}
interface Ward {
code: number // 1–1450
name: string
slug: string
constituencyCode: number
countyCode: number
subCounty: string | null // sub-county slug; null for 12 unmatched wards
aliases: string[]
}
interface SubCounty {
slug: string // primary key; these units have no official code
name: string
countyCode: number
constituencyCode: number | null // same-name constituency, if any
wardCodes: number[]
}Also exported: Country, Province, EconomicBloc, CountyNode, ConstituencyNode, SearchResult, RegionOption, WardLineage, Level, Query, AsalClass, ProvinceCode, BlocCode and the country sub-types.
In practice
Recipes
Dependent county → constituency → ward selects
const [county, setCounty] = useState<string>()
const [constituency, setConstituency] = useState<string>()
<select onChange={(e) => { setCounty(e.target.value); setConstituency(undefined) }}>
<option value="">Select a county</option>
{countyOptions().map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
<select disabled={!county} onChange={(e) => setConstituency(e.target.value)}>
{constituencyOptions({ county: Number(county) }).map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
<select disabled={!constituency}>
{wardOptions({ constituency: Number(constituency) }).map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>Validating a submitted address
function validate(countyCode: number, wardCode: number) {
const lineage = getWardLineage(wardCode)
if (!lineage) return 'Unknown ward'
if (lineage.county.code !== countyCode) {
return `That ward is in ${lineage.county.name}, not the county selected`
}
return null
}Joining against a dataset that uses ISO codes
// A dataset keyed 'KE-01'…'KE-47' must go through isoToCounty,
// never getCounty(Number(...)): the two numbers are unrelated.
const rows = external.map((row) => ({
...row,
county: isoToCounty(row.iso),
}))Grouping counties for a regional report
const byProvince = provinces.map((province) => ({
province: province.name,
counties: getCountiesByProvince(province.code),
population: getCountiesByProvince(province.code)
.reduce((total, county) => total + county.population[2019], 0),
}))In practice
Bundle size
Import from a subpath and nothing else is bundled. These are real measurements, taken with esbuild, minified, from a clean install of the packed tarball:
| Import | Minified | Gzipped |
|---|---|---|
from 'kenya-regions/counties' | 14.6 KB | 3.5 KB |
from 'kenya-regions' | 93.4 KB | 26.2 KB |
Six times smaller for the same data, in the commonest case of all: a county dropdown.
| Entry point | Size | Contains |
|---|---|---|
kenya-regions/blocs | ~2 KB | 7 blocs |
kenya-regions/provinces | ~2 KB | 8 provinces |
kenya-regions/country | ~5 KB | country record + helpers |
kenya-regions/outlines | ~53 KB | 47 coarse county outlines + point lookup |
kenya-regions/districts | ~22 KB | 158 districts |
kenya-regions/counties | ~25 KB | 47 counties |
kenya-regions/wards | ~48 KB | 1450 wards |
kenya-regions/subcounties | ~57 KB | 301 sub-counties |
kenya-regions/constituencies | ~70 KB | 290 constituencies |
kenya-regions/locations | ~71 KB | 2723 locations |
kenya-regions/divisions | ~81 KB | 635 divisions |
kenya-regions | ~211 KB | everything except the four census levels |
kenya-regions/sublocations | ~429 KB | 7150 sub-locations with census figures |
How the large datasets are stored
Wards, locations and sub-locations ship as arrays of tuples, rebuilt into objects on import. Past a few thousand records the repeated JSON key names cost more than the values do. "formerProvinceCode":"RFT", is about 28 bytes on every row, so dropping the keys is the single biggest saving available.
| Dataset | As objects | Packed | Saving |
|---|---|---|---|
| Wards | 180 KB | 35 KB | 5.2× |
| Locations | 303 KB | 59 KB | 5.1× |
| Sub-locations | 1,684 KB | 357 KB | 4.7× |
Fields that can be derived are not stored at all: slug is computed from the name, and densityPerKm2 is recomputed as population over area. Rehydration costs a few milliseconds at import and is invisible to callers: the exported arrays are ordinary typed objects.
Subpaths cannot resolve parent names
kenya-regions/wards does not bundle the county dataset, so it cannot turn 'Kiambu' into a code. Rather than returning an empty array that looks like a county with no wards, it throws and tells you to pass the numeric code or use the main entry.
Raw JSON is published too:
import counties from 'kenya-regions/data/counties.json' with { type: 'json' }In practice
Where the data comes from
Sources are committed in the repository under data/sources/, and npm run build:data regenerates every dataset from them offline. No figure in the package is unattributable.
| Source | Provides |
|---|---|
| IEBC county / constituency / ward hierarchy | The 47 / 290 / 1450 backbone and all three code sets |
| OCHA COD-AB for Kenya | P-codes, areas, centroids, former names |
| IEBC 2013 boundary shapefile | Independent cross-check of ward names |
| KNBS sub-county / ward listing | The 301 administrative sub-counties |
| KNBS 2009 census, population by sub-location | Districts, divisions, locations, sub-locations and their census figures |
| OCHA COD admin1 boundaries | Coarse county outlines and each county bbox |
| KNBS 2019 and 2009 censuses | Population |
| ISO 3166-1 and 3166-2:KE | Country and county codes, withdrawn province codes |
| UN M49 | Kenya’s place in the world statistical hierarchy |
| ASAL policy / NDMA | Arid and semi-arid classification |
| IGRTC / Council of Governors | Economic bloc membership |
| Constitution, Articles 97, 98, 177 | Parliamentary seat counts |
How it is validated
The build refuses to emit anything unless all of the following hold, and the same assertions run again in the test suite on every commit:
- exactly 47 counties, 290 constituencies and 1450 wards
- codes form gapless 1–n sequences with no duplicates at every level
- every constituency belongs to a real county, and every ward agrees with its constituency about which county it is in
- no county without constituencies, no constituency without wards
- county → constituency assignment agrees between two independent sources
- ISO codes, p-codes and slugs are unique
- 23 ASAL counties, 9 of them arid
- 301 sub-counties spanning all 47 counties, none without wards, every ward in the same county as its sub-county, and the ward back-reference round-trips
- 158 districts, 635 divisions, 2723 locations and 7150 sub-locations, each level agreeing with its parent about its ancestry, none childless, and sub-location populations summing to the published 2009 national total
- the country record agrees with the datasets: population equals the sum of the counties, the capital resolves to a real county, every county code extends the country’s, and each chamber’s seats add up
The strongest check
County populations must sum to the published KNBS national totals: 47,564,296 for 2019 and 38,610,097 for 2009. Because both reconcile exactly, all 47 county figures are independently confirmed. This check caught a real error during the rebuild: Turkana had been recorded as 1,504,976 when the census figure is 926,976.
In practice
Known limitations
Stated plainly rather than papered over, so you can judge whether they affect your use.
Ward name variants
Official sources spell some ward names differently: Wargadud / Wargudud, Ndavaya / Nadavaya. Where the variants are clearly the same place, the alternative is attached as a searchable alias, so lookups succeed either way.
27 ward code conflicts
For 27 of the 1450 wards, two official sources disagree about which numeric code belongs to which ward within the same constituency. County and constituency membership is unaffected, so no lookup returns a wrong parent. They are listed in data/sources/name-conflicts.json rather than silently resolved in favour of whichever source loaded first.
Ward names are not unique nationally
Several counties have a “Township” or a “Central” ward. Use codes, or scope by constituency, when it matters.
12 wards have no sub-county
The sub-county source spells them differently enough that no confident match was possible, so ward.subCounty is null rather than guessed, giving 99.2% coverage. They are listed in data/sources/name-conflicts.json, and getWardsByCounty is always complete.
The sub-county count is behind
301 ships against a current figure of about 341. See the sub-counties section for why the gap is documented rather than guessed at.
The census hierarchy is a 2009 snapshot
Districts no longer exist as administrative units, and divisions, locations and sub-locations have been gazetted since.
ASAL is recorded at county level
The underlying reality is sub-county level: some ASAL areas sit inside counties that are otherwise not ASAL.
Two national areas that do not reconcile
kenya.area.totalKm2 is 580,367 km², the internationally cited figure. The gazetted county areas sum to roughly 591,346 km². The two are measured differently, so the build does not assert they match.
No administrative sub-counties
Covered above: the term is ambiguous, so neither set is shipped under that name.