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.

v2.2.0 MIT Zero dependencies ESM + CJS TypeScript Node ≥ 18
47Counties
290Constituencies
1,450Wards
301Sub-counties
7,150Sub-locations
8Provinces
0Network calls

Getting started

Install

shell
npm install kenya-regions

No 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

typescript
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 name

CommonJS works identically:

javascript
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 · devolved

Created by the 2010 Constitution. Each has a governor, a senator and a county assembly. The primary modern unit.

Constituencies
290 · electoral

Each elects one member of the National Assembly. Fixed at 290 by the Constitution itself.

Wards
1450 · electoral

County assembly wards, each electing one MCA. The finest unit in the electoral hierarchy.

Administrative sub-counties
301 · national gov’t

Run by Deputy County Commissioners. A second hierarchy that shares a name with the constituency but not its boundaries.

Former provinces
8 · historical

Abolished in 2013, still everywhere in older data and everyday speech. Partitions the country cleanly.

ISO 3166-2:KE
47 · international

The same counties, numbered alphabetically. Different numbers for the same places.

OCHA p-codes
humanitarian

Used by HDX, ReliefWeb and IPC. These do follow the constitutional numbering.

Economic blocs
7 · voluntary

County groupings under Article 189(2). They overlap, and they do not cover the whole country.

ASAL classification
23 · functional

Arid 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.

County 1–47 First Schedule order: starts at the coast (Mombasa is 1) and ends with Nairobi (47). Elects a governor, a senator and a county woman representative.
Constituency 1–290 IEBC delimitation. Elects one member of the National Assembly.
Ward 1–1450 County assembly ward. Elects one member of a county assembly.

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.

SenseThe unitCountExportHeaded by
County governmentThe constituency290constituenciesElected MP
National governmentAdministrative sub-county301*subCountiesDeputy 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:

typescript
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, Tiaty

Building 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:

District 158Superseded by counties in 2013. Kept for joining against census-era data.
Division 635Headed by an Assistant County Commissioner.
Location 2,723Headed by a Chief.
Sub-location 7,150Headed by an Assistant Chief. The level the census enumerates at.
typescript
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].densityPerKm2

Sub-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.

CountyConstitutional codeISO 3166-2OCHA p-code
Mombasa1KE-28KE001
Baringo30KE-01KE030
Nairobi47KE-30KE047
the trap
getCounty(30).name        // 'Baringo'
getCounty('KE-30').name   // 'Nairobi'  ← a different county entirely

Both 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.

47 counties, shaded by the province each was carved out of. Simplified to roughly a kilometre.

Open the atlas →

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.

ExportTypeRecordsOrder
kenyaCountry1
countiesreadonly County[]47Constitutional code
countiesByNamereadonly County[]47Alphabetical
constituenciesreadonly Constituency[]290IEBC code
wardsreadonly Ward[]1450IEBC code
subCountiesreadonly SubCounty[]301County, then name
provincesreadonly Province[]8Conventional
blocsreadonly EconomicBloc[]7Membership size

Raw JSON is published too, for anyone who wants the data without the API:

typescript
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 byForms
getCountycode, padded code, ISO code, p-code, name, slug, former name
getConstituencycode, padded code, p-code, name, slug, former name
getWardcode, padded code, name, slug, spelling variant

getCounty(query: Query): County | undefined

Resolves a county by any of its identifiers. Returns undefined rather than guessing.

typescript
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')       // undefined

getConstituency(query: Query): Constituency | undefined

Resolves a constituency, including by names retired in the 2013 delimitation.

typescript
getConstituency('Mbita')    // → Suba North
getConstituency('Gwasi')    // → Suba South
getConstituency('Dujis')    // → Garissa Township
getConstituency('Kilimani') // → Dagoretti North

getWard(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.

typescript
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.

typescript
getCountiesByProvince('Rift Valley').length  // 14
getCountiesByProvince('RFT').length          // 14, the code works too

getCountiesByBloc(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.

CodeBlocCounties
LREBLake Region Economic Bloc14
MKAREBMt. Kenya and Aberdares Region Economic Bloc10
NOREBNorth Rift Economic Bloc8
FCDCFrontier Counties Development Council7
JKPJumuiya ya Kaunti za Pwani6
NAMETRONairobi Metropolitan Area5
SEKEBSouth Eastern Kenya Economic Bloc3

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.

typescript
getAsalCounties()            // 23
getAsalCounties('arid')      // 9
getAsalCounties('semi-arid') // 14

This 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.

typescript
search('kis', { levels: ['county'], limit: 5 })
search('mbita')                      // → Suba North, matched: 'Mbita'
search('nairobi', { levels: ['ward'] })
OptionTypeDefault
levelsLevel[]all three
limitnumber10
thresholdnumber (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.

typescript
countyOptions()
// [{ label: 'Baringo', value: '30', region: {…} }, …]  alphabetical

countyOptions({ alphabetical: false, valueKey: 'slug' })
constituencyOptions({ county: 47 })
wardOptions({ constituency: 'Westlands' })
OptionApplies toDefault
alphabeticalalltrue
valueKeyall'code'
countyconstituency, ward
constituencyward

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

typescript
countyToIso(47)          // 'KE-30'
isoToCounty('KE-30')     // Nairobi
countyToPcode(47)        // 'KE047'

fromPcode('KE047')       // Nairobi county
fromPcode('KE047275')    // Dagoretti North constituency

isoToCounty 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.

typescript
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

statistical hierarchy
001 World → 002 Africa → 202 Sub-Saharan Africa → 014 Eastern Africa → 404 Kenya

Derived, 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.

ChamberSeatsComposition
National Assembly349290 constituency + 47 county woman rep + 12 nominated
Senate6747 elected + 16 women + 2 youth + 2 PWD
County assemblies1450one 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

typescript
toInternationalPhone('0712 345 678')  // '+254712345678'
toInternationalPhone('12345')         // undefined
formatCurrency(1234.5)                // 'KSh 1,234.50'
isPostalCode('00100')                 // true

formatCurrency 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.

typescript
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 Kenya

Arguments 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:

typescript
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

typescript
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

tsx
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

typescript
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

typescript
// 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

typescript
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:

ImportMinifiedGzipped
from 'kenya-regions/counties'14.6 KB3.5 KB
from 'kenya-regions'93.4 KB26.2 KB

Six times smaller for the same data, in the commonest case of all: a county dropdown.

Entry pointSizeContains
kenya-regions/blocs~2 KB7 blocs
kenya-regions/provinces~2 KB8 provinces
kenya-regions/country~5 KBcountry record + helpers
kenya-regions/outlines~53 KB47 coarse county outlines + point lookup
kenya-regions/districts~22 KB158 districts
kenya-regions/counties~25 KB47 counties
kenya-regions/wards~48 KB1450 wards
kenya-regions/subcounties~57 KB301 sub-counties
kenya-regions/constituencies~70 KB290 constituencies
kenya-regions/locations~71 KB2723 locations
kenya-regions/divisions~81 KB635 divisions
kenya-regions~211 KBeverything except the four census levels
kenya-regions/sublocations~429 KB7150 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.

DatasetAs objectsPackedSaving
Wards180 KB35 KB5.2×
Locations303 KB59 KB5.1×
Sub-locations1,684 KB357 KB4.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:

typescript
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.

SourceProvides
IEBC county / constituency / ward hierarchyThe 47 / 290 / 1450 backbone and all three code sets
OCHA COD-AB for KenyaP-codes, areas, centroids, former names
IEBC 2013 boundary shapefileIndependent cross-check of ward names
KNBS sub-county / ward listingThe 301 administrative sub-counties
KNBS 2009 census, population by sub-locationDistricts, divisions, locations, sub-locations and their census figures
OCHA COD admin1 boundariesCoarse county outlines and each county bbox
KNBS 2019 and 2009 censusesPopulation
ISO 3166-1 and 3166-2:KECountry and county codes, withdrawn province codes
UN M49Kenya’s place in the world statistical hierarchy
ASAL policy / NDMAArid and semi-arid classification
IGRTC / Council of GovernorsEconomic bloc membership
Constitution, Articles 97, 98, 177Parliamentary 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.