Kenyan administrative divisions — counties, sub-counties, constituencies, wards, localities, and areas — packaged as a fast, well-typed library for JavaScript/TypeScript, React, Kotlin/JVM, Swift, Dart/Flutter, and PHP.
47 counties · 307 sub-counties · 290 constituencies · 1,448 wards · 916 localities · 1,829 areas
| Platform | Package | Install |
|---|---|---|
| JavaScript / TypeScript | kenya-locations |
npm install kenya-locations |
| React | kenya-locations-react |
npm install kenya-locations-react |
| Kotlin / JVM (Android, Spring Boot, etc.) | io.github.davidamunga:kenya-locations |
See below |
| Swift (iOS, macOS, tvOS, watchOS) | KenyaLocations |
See below |
| Dart / Flutter | kenya_locations |
See below |
| PHP | davidamunga/kenya-locations |
See below |
All packages are built from the same JSON source data and share identical version numbers (except kenya-locations-react, kenya_locations, and davidamunga/kenya-locations, which version independently).
npm install kenya-locations
# pnpm add kenya-locations
# yarn add kenya-locationsnpm install kenya-locations-react
# peer deps: react >=18, kenya-locations// build.gradle.kts
dependencies {
implementation("io.github.davidamunga:kenya-locations:0.5.4")
}Works on Android, Spring Boot, Ktor, CLI tools, and any JVM project. No Android SDK or special initialisation required — data loads lazily from the JAR classpath on first access.
In Xcode: File → Add Package Dependencies and enter https://github.com/DavidAmunga/kenya-locations, or add to Package.swift:
dependencies: [
.package(url: "https://github.com/DavidAmunga/kenya-locations", from: "0.5.0"),
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "KenyaLocations", package: "kenya-locations"),
]
),
]Requires Swift 5.9+ / iOS 13+ / macOS 10.15+. Data loads lazily from the app bundle on first access.
flutter pub add kenya_locations
# dart pub add kenya_locationsdependencies:
kenya_locations: ^0.1.1Works in any Dart or Flutter project. No initialisation required — data is compiled into the package as Dart constants and available immediately.
composer require davidamunga/kenya-locationsWorks in any PHP 8.2+ project, including WordPress themes and plugins. No initialisation required — JSON loads lazily from the package on first access. A drop-in example plugin is in examples/wordpress.
| App | Path | Uses |
|---|---|---|
| Android (Compose) | examples/android |
Kotlin library (packages/kotlin) |
| Flutter | examples/flutter |
Dart library (packages/dart) |
| WordPress | examples/wordpress |
PHP library (packages/php); zip on each v* GitHub release |
import {
getCounties,
county,
getConstituenciesInCounty,
getConstituencyOfWard,
search,
DATA_VERSION,
} from "kenya-locations";
// All 47 counties with rich metadata
const counties = getCounties();
console.log(counties[0].capital); // "Nairobi"
console.log(counties[0].population_2019); // 4397073
console.log(counties[0].region); // "Nairobi"
// Chainable wrapper API
const nairobi = county("Nairobi");
const westlands = nairobi?.constituency("Westlands");
const wards = westlands?.wards();
// Standalone functions (tree-shakeable)
const cs = getConstituenciesInCounty("Nairobi"); // ConstituencyWrapper[]
const parentCs = getConstituencyOfWard("Mountain View"); // ConstituencyWrapper
// Fuzzy search — typo-tolerant, results sorted by relevance
const results = search("karen", { limit: 10, types: ["locality", "area"] });import { useCounty, useConstituenciesInCounty, useSearch } from "kenya-locations-react";
function LocationPicker() {
const nairobi = useCounty("Nairobi");
const constituencies = useConstituenciesInCounty("Nairobi");
const [query, setQuery] = useState("");
const { results, isPending } = useSearch(query, {
types: ["constituency", "ward"],
debounceMs: 300,
});
return (/* ... */);
}// No init() required — just call directly
val counties = KenyaLocations.getCounties()
val nairobi = KenyaLocations.getCountyByName("Nairobi")
println(nairobi?.capital) // "Nairobi"
println(nairobi?.population_2019) // 4397073
val wards = KenyaLocations.getWardsInConstituency("Westlands")
val localities = KenyaLocations.getLocalitiesInCounty("Nairobi")
// Fuzzy search — tolerates typos, sorted by relevance
val results = KenyaLocations.search("Nairob") // matches "Nairobi"
// Java
List<County> counties = KenyaLocations.INSTANCE.getCounties();import KenyaLocations
let kl = KenyaLocations.shared
let counties = kl.getCounties()
print(counties[0].capital) // "Nairobi"
print(counties[0].population_2019) // 4397073
let wards = kl.getWardsInConstituency("Westlands")
let localities = kl.getLocalitiesInCounty("Nairobi")
// Fuzzy search — tolerates typos, sorted by relevance
let results = kl.search("Nairob", limit: 10) // matches "Nairobi"import 'package:kenya_locations/kenya_locations.dart';
final counties = KenyaLocations.getCounties();
final nairobi = KenyaLocations.getCountyByName('Nairobi');
print(nairobi?.capital); // Nairobi
print(nairobi?.population2019); // 4397073
final wards = KenyaLocations.getWardsInConstituency('Westlands');
final localities = KenyaLocations.getLocalitiesInCounty('Nairobi');
// Fuzzy search — tolerates typos, sorted by relevance
final results = KenyaLocations.search('Nairob', limit: 20); // matches "Nairobi"use KenyaLocations\KenyaLocations;
$counties = KenyaLocations::getCounties();
$nairobi = KenyaLocations::getCountyByName('Nairobi');
echo $nairobi?->capital; // Nairobi
echo $nairobi?->population2019; // 4397073
$wards = KenyaLocations::getWardsInConstituency('Westlands');
$localities = KenyaLocations::getLocalitiesInCounty('Nairobi');
// Fuzzy search — tolerates typos, sorted by relevance
$results = KenyaLocations::search('Nairob', limit: 20); // matches "Nairobi"County (47)
├── Metadata: capital, area_km2, population_2019, region, postal_code
├── Locality → Area (informal addressing: estates, neighbourhoods)
│ 916 localities · 1,829 areas
└── Constituency → Ward (electoral / administrative)
290 constituencies · 1,448 wards
└── Sub-County (307)
Import only what you need for optimal tree-shaking:
| Module | Exports |
|---|---|
kenya-locations/counties |
getCounties, getCountyByCode, getCountyByName, county |
kenya-locations/constituencies |
getConstituencies, getConstituencyByCode, getConstituencyByName, getConstituenciesInCounty |
kenya-locations/wards |
getWards, getWardByCode, getWardByName, getWardsInCounty, getWardsInConstituency, getConstituencyOfWard, getSubCountyOfWard |
kenya-locations/sub-counties |
getSubCounties, getSubCountiesInCounty, getSubCountyByCode, getSubCountyByName, getWardsInSubCounty |
kenya-locations/localities |
getLocalities, getLocalityByName, getLocalitiesByName, getLocalitiesInCounty, locality |
kenya-locations/areas |
getAreas, getAreaByName, getAreasByName, getAreasInLocality, getAreasInCounty |
kenya-locations/search |
search, searchByType |
kenya-locations/version |
DATA_VERSION |
Or import everything from the root entry point:
import { getCounties, getConstituenciesInCounty, search, county, DATA_VERSION } from "kenya-locations";Every County object now carries rich KNBS-sourced metadata:
import { getCountyByCode, getCounties } from "kenya-locations";
const nairobi = getCountyByCode("047");
console.log(nairobi?.capital); // "Nairobi"
console.log(nairobi?.area_km2); // 694.9
console.log(nairobi?.population_2019); // 4397073
console.log(nairobi?.region); // "Nairobi" (former province)
console.log(nairobi?.postal_code); // "00100"
// Sort by population
const byPop = getCounties().sort((a, b) => b.population_2019 - a.population_2019);import { county } from "kenya-locations/counties";
import { locality } from "kenya-locations/localities";
// County → drill down
county("Nairobi")?.constituencies(); // ConstituencyWrapper[]
county("Nairobi")?.constituency("Westlands"); // ConstituencyWrapper
county("Nairobi")?.localities(); // LocalityWrapper[]
county("Nairobi")?.locality("Westlands"); // LocalityWrapper
county("Nairobi")?.areas(); // Area[]
county("047")?.wards(); // Ward[]
// Constituency → wards
county("Nairobi")?.constituency("Westlands")?.wards();
// Locality → areas
locality("Westlands")?.areas();
locality("Westlands")?.area("Gigiri");
locality("Westlands")?.getCounty();import {
getConstituencyOfWard,
getSubCountyOfWard,
getCountyOfWard,
getCountyOfConstituency,
getCountyOfLocality,
getLocalityOfArea,
} from "kenya-locations";
// Resolve a ward's parents
const constituency = getConstituencyOfWard("Mountain View");
// → ConstituencyWrapper { name: "Westlands", county: "Nairobi" }
const subCounty = getSubCountyOfWard("Mountain View");
// → SubCounty { name: "Westlands", county: "Nairobi" }
const county = getCountyOfWard("Mountain View");
// → County { name: "Nairobi", ... }Search is fuzzy and typo-tolerant on all platforms. The JS package uses Fuse.js (Bitap algorithm, threshold: 0.4); Kotlin and Swift use the same effective behaviour via a Levenshtein sliding-window implementation.
import { search, searchByType } from "kenya-locations/search";
// Fuzzy search all types — "karen" → Karen area, Karen locality, …
const results = search("karen");
// Limit and type-filter
const counties = search("nairobi", { types: ["county"], limit: 5 });
const localities = searchByType("west", "locality", 10);SearchResult is a discriminated union — TypeScript narrows item automatically:
import type { SearchResult } from "kenya-locations";
for (const result of search("Westlands")) {
if (result.type === "county") {
console.log(result.item.capital); // County — capital is available
} else if (result.type === "ward") {
console.log(result.item.constituency); // Ward — constituency is available
} else if (result.type === "area") {
console.log(result.item.locality); // Area — locality is available
}
}type CountyRegion =
| "Nairobi" | "Central" | "Coast" | "Eastern"
| "North Eastern" | "Nyanza" | "Rift Valley" | "Western";
interface County {
code: string;
name: string;
capital: string;
area_km2: number;
population_2019: number;
region: CountyRegion;
postal_code: string;
}
interface SubCounty { code: string; name: string; county: string; }
interface Constituency { code: string; name: string; county: string; }
interface Ward { code: string; name: string; constituency: string; }
interface Locality { name: string; county: string; }
interface Area { name: string; locality: string; county: string; }
// Discriminated union — item type is narrowed automatically by TypeScript
type SearchResult =
| { type: "county"; item: County }
| { type: "constituency"; item: Constituency }
| { type: "ward"; item: Ward }
| { type: "sub-county"; item: SubCounty }
| { type: "locality"; item: Locality }
| { type: "area"; item: Area };import { LocationNotFoundError, LocationError } from "kenya-locations";
try {
county("Nairobi")?.locality("NonExistent"); // throws LocationNotFoundError
} catch (e) {
if (e instanceof LocationNotFoundError) { /* ... */ }
}Error hierarchy: LocationError → LocationNotFoundError, InvalidLocationCodeError, SearchError, DataValidationError, ConfigurationError
Memoised hooks for every level of the hierarchy. All hooks return stable array references across renders.
npm install kenya-locations-react
# peer deps: react >=18, kenya-locations| Hook | Returns | Description |
|---|---|---|
useCounties() |
County[] |
All 47 counties |
useCounty(nameOrCode) |
CountyWrapper | undefined |
Single county wrapper |
useConstituencies() |
Constituency[] |
All 290 constituencies |
useConstituency(codeOrName) |
ConstituencyWrapper | undefined |
Single constituency |
useConstituenciesInCounty(nameOrCode) |
ConstituencyWrapper[] |
Constituencies in a county |
useWards() |
Ward[] |
All 1,448 wards |
useWardsInCounty(nameOrCode) |
Ward[] |
Wards in a county |
useWardsInConstituency(nameOrCode) |
Ward[] |
Wards in a constituency |
useConstituencyOfWard(nameOrCode) |
ConstituencyWrapper | undefined |
Parent constituency of a ward |
useSubCountyOfWard(nameOrCode) |
SubCounty | undefined |
Parent sub-county of a ward |
useSubCounties() |
SubCounty[] |
All 307 sub-counties |
useSubCountiesInCounty(nameOrCode) |
SubCounty[] |
Sub-counties in a county |
useLocalities() |
Locality[] |
All 916 localities |
useLocalitiesInCounty(countyName) |
Locality[] |
Localities in a county |
useLocality(name, countyName?) |
LocalityWrapper | undefined |
Single locality (county-scoped) |
useAreas() |
Area[] |
All 1,829 areas |
useAreasInLocality(localityName) |
Area[] |
Areas in a locality |
useAreasInCounty(countyName) |
Area[] |
Areas in a county |
useSearch(query, options?) |
{ results: SearchResult[], isPending: boolean } |
Debounced fuzzy search |
useSearch(query, {
limit?: number; // max results (default 10)
types?: SearchType[]; // restrict to specific entity types
debounceMs?: number; // debounce delay in ms (default 300, set 0 to disable)
})isPending is true while the debounce timer is running (query changed but search hasn't fired yet). results is typed as SearchResult[] — the discriminated union means TypeScript narrows item automatically.
KenyaLocations.getCounties() // List<County>
KenyaLocations.getCountyByCode("047") // County?
KenyaLocations.getCountyByName("Nairobi") // County?
KenyaLocations.getSubCounties() // List<SubCounty>
KenyaLocations.getConstituencies() // List<Constituency>
KenyaLocations.getConstituencyByCode("290") // Constituency?
KenyaLocations.getConstituencyByName("Westlands") // Constituency?
KenyaLocations.getWards() // List<Ward>
KenyaLocations.getLocalities() // List<Locality>
KenyaLocations.getAreas() // List<Area>KenyaLocations.getSubCountiesInCounty("Nairobi")
KenyaLocations.getConstituenciesInCounty("Nairobi")
KenyaLocations.getWardsInConstituency("Westlands")
KenyaLocations.getWardsInCounty("Nairobi")
KenyaLocations.getLocalitiesInCounty("Nairobi")
KenyaLocations.getAreasInLocality("Karen")
KenyaLocations.getAreasInCounty("Nairobi")Search is fuzzy and typo-tolerant — a query like "Nairob" matches "Nairobi". Results are sorted by relevance (best match first).
KenyaLocations.search("karen", limit = 20) // List<SearchResult<*>>
KenyaLocations.searchByType("west", SearchType.WARD) // List<SearchResult<*>>data class County(
val code: String,
val name: String,
val capital: String,
val area_km2: Double,
val population_2019: Long,
val region: String, // former province, e.g. "Nairobi", "Central"
val postal_code: String,
)
data class SubCounty(val code: String, val name: String, val county: String)
data class Constituency(val code: String, val name: String, val county: String)
data class Ward(val code: String, val name: String, val constituency: String)
data class Locality(val name: String, val county: String)
data class Area(val name: String, val locality: String, val county: String)
enum class SearchType { COUNTY, SUB_COUNTY, CONSTITUENCY, WARD, LOCALITY, AREA }
data class SearchResult<T>(val type: SearchType, val item: T)let kl = KenyaLocations.shared
kl.getCounties() // [County]
kl.getCountyByCode("047") // County?
kl.getCountyByName("Nairobi") // County?
kl.getSubCounties() // [SubCounty]
kl.getConstituencies() // [Constituency]
kl.getConstituencyByCode("290") // Constituency?
kl.getConstituencyByName("Westlands") // Constituency?
kl.getWards() // [Ward]
kl.getLocalities() // [Locality]
kl.getAreas() // [Area]kl.getSubCountiesInCounty("Nairobi")
kl.getConstituenciesInCounty("Nairobi")
kl.getWardsInConstituency("Westlands")
kl.getWardsInCounty("Nairobi")
kl.getLocalitiesInCounty("Nairobi")
kl.getAreasInLocality("Karen")
kl.getAreasInCounty("Nairobi")Search is fuzzy and typo-tolerant — a query like "Nairob" matches "Nairobi". Results are sorted by relevance (best match first).
kl.search("karen", limit: 20) // [SearchResult]
kl.searchByType("west", type: .ward) // [SearchResult]
// SearchResult is an enum with associated values
for result in kl.search("Westlands") {
switch result {
case .county(let c): print(c.capital)
case .constituency(let c): print(c.county)
case .ward(let w): print(w.constituency)
case .locality(let l): print(l.county)
case .area(let a): print(a.locality)
case .subCounty(let s): print(s.county)
}
}public struct County: Codable {
public let code: String
public let name: String
public let capital: String
public let area_km2: Double
public let population_2019: Int
public let region: CountyRegion // enum: .nairobi, .central, .coast, …
public let postal_code: String
}
public struct SubCounty: Codable { let code, name, county: String }
public struct Constituency: Codable { let code, name, county: String }
public struct Ward: Codable { let code, name, constituency: String }
public struct Locality: Codable { let name, county: String }
public struct Area: Codable { let name, locality, county: String }
public enum CountyRegion: String, Codable, CaseIterable {
case nairobi, central, coast, eastern, northEastern,
nyanza, riftValley, western
}KenyaLocations.getCounties(); // List<County>
KenyaLocations.getCountyByCode('047'); // County?
KenyaLocations.getCountyByName('Nairobi'); // County?
KenyaLocations.getSubCounties(); // List<SubCounty>
KenyaLocations.getConstituencies(); // List<Constituency>
KenyaLocations.getWards(); // List<Ward>
KenyaLocations.getLocalities(); // List<Locality>
KenyaLocations.getAreas(); // List<Area>KenyaLocations.getSubCountiesInCounty('Nairobi');
KenyaLocations.getConstituenciesInCounty('Nairobi');
KenyaLocations.getWardsInConstituency('Westlands');
KenyaLocations.getLocalitiesInCounty('Nairobi');
KenyaLocations.getAreasInLocality('Karen');
KenyaLocations.getConstituencyOfWard('Mountain view'); // Constituency?
KenyaLocations.getConstituencyOfWard('1370'); // same ward, by codeSearch is fuzzy and typo-tolerant — a query like 'Nairob' matches 'Nairobi', using the same Levenshtein sliding-window approach as the Kotlin and Swift libraries. Results are sorted by relevance (best match first).
final results = KenyaLocations.search('karen', limit: 20); // List<SearchResult<dynamic>>
final wardsOnly = KenyaLocations.searchByType('West', SearchType.ward);
for (final result in results) {
switch (result.type) {
case SearchType.county:
print((result.item as County).capital);
case SearchType.ward:
print((result.item as Ward).constituency);
case SearchType.area:
print((result.item as Area).locality);
default:
break;
}
}class County {
final String code;
final String name;
final String capital;
final double areaKm2;
final int population2019;
final String region;
final String postalCode;
}
class SubCounty { final String code, name, county; }
class Constituency { final String code, name, county; }
class Ward { final String code, name, constituency; }
class Locality { final String name, county; }
class Area { final String name, locality, county; }
enum SearchType { county, subCounty, constituency, ward, locality, area }
class SearchResult<T> { final SearchType type; final T item; String get name; }KenyaLocations::getCounties(); // list<County>
KenyaLocations::getCountyByCode('047'); // ?County
KenyaLocations::getCountyByName('Nairobi'); // ?County
KenyaLocations::getSubCounties(); // list<SubCounty>
KenyaLocations::getConstituencies(); // list<Constituency>
KenyaLocations::getWards(); // list<Ward>
KenyaLocations::getLocalities(); // list<Locality>
KenyaLocations::getAreas(); // list<Area>KenyaLocations::getSubCountiesInCounty('Nairobi');
KenyaLocations::getConstituenciesInCounty('047'); // name or code
KenyaLocations::getWardsInConstituency('Westlands');
KenyaLocations::getWardsInConstituency('274');
KenyaLocations::getWardsInCounty('Nairobi');
KenyaLocations::getWardsInSubCounty('Ainabkoi');
KenyaLocations::getLocalitiesInCounty('Nairobi');
KenyaLocations::getAreasInLocality('Karen');
KenyaLocations::getConstituencyOfWard('Mountain view'); // ?Constituency
KenyaLocations::getConstituencyOfWard('1370'); // same ward, by code
KenyaLocations::getCountyOfWard('1370');
KenyaLocations::getLocality('Westlands', 'Nairobi');
KenyaLocations::getLocalitiesByName('Westlands');
KenyaLocations::getLocalityOfArea('Gigiri');Search is fuzzy and typo-tolerant — a query like 'Nairob' matches 'Nairobi', using the same Levenshtein sliding-window approach as the Kotlin, Swift, and Dart libraries. Results are sorted by relevance (best match first).
$results = KenyaLocations::search('karen', limit: 20);
$wardsOnly = KenyaLocations::searchByType('West', SearchType::Ward);
foreach ($results as $result) {
echo $result->type->value . ': ' . $result->name() . PHP_EOL;
}final readonly class County {
public string $code;
public string $name;
public string $capital;
public float $areaKm2;
public int $population2019;
public string $region;
public string $postalCode;
}
final readonly class SubCounty { public string $code, $name, $county; }
final readonly class Constituency { public string $code, $name, $county; }
final readonly class Ward { public string $code, $name, $constituency; }
final readonly class Locality { public string $name, $county; }
final readonly class Area { public string $name, $locality, $county; }
enum SearchType: string { case County; case SubCounty; case Constituency; case Ward; case Locality; case Area; }
final readonly class SearchResult { public SearchType $type; public object $item; public function name(): string; }Contributions are very welcome — especially data additions (new localities, areas, corrections).
Data files are plain JSON in data/. No TypeScript or Kotlin knowledge needed to add entries. The pre-commit hook validates data automatically on every commit.
data/
├── counties.json ← includes capital, area_km2, population_2019, region, postal_code
├── sub-counties.json
├── constituencies.json
├── wards.json
├── locality.json
└── area.json
See packages/js/CONTRIBUTING.md for data structure, validation rules, and submission guidelines. Submit new areas via the web app (opens a GitHub issue) or a pull request.
After editing data/*.json, regenerate the Dart constants from the repo root with dart run packages/dart/scripts/generate_data.dart, and refresh the PHP copy with php packages/php/scripts/copy-data.php.
Commits and PR titles use Conventional Commits (plus a data type for JSON updates). See AGENTS.md.
Releasable changes (data or package source) need a changeset (pnpm changeset). Versioning and publish steps are in RELEASING.md.
The interactive demo lives in apps/web and is published at kenya-locations.web.app. The UI is built with coss ui on Base UI.
pnpm install
pnpm start # http://localhost:3000It uses the local kenya-locations workspace package, so library changes show up immediately. Area submissions open a prefilled GitHub issue — no .env needed.
kenya-locations/
├── data/ ← shared JSON source of truth (all libraries read from here)
├── packages/
│ ├── js/ ← TypeScript library → npm: kenya-locations
│ ├── react/ ← React hooks library → npm: kenya-locations-react
│ ├── kotlin/ ← Kotlin/JVM library → Maven: io.github.davidamunga:kenya-locations
│ ├── swift/ ← Swift library → Swift Package Index: KenyaLocations
│ ├── dart/ ← Dart/Flutter library → pub.dev: kenya_locations
│ └── php/ ← PHP library → Packagist: davidamunga/kenya-locations
├── apps/
│ └── web/ ← interactive demo (kenya-locations.web.app)
├── examples/
│ ├── android/ ← Compose app using the Kotlin library
│ ├── flutter/ ← Flutter app using the Dart library (packages/dart)
│ └── wordpress/ ← WordPress plugin using the PHP library (packages/php)
└── scripts/
└── validate-data.js ← data integrity checks (runs on commit + CI)
MIT © David Amunga
Data sourced from the Independent Electoral and Boundaries Commission (IEBC) and Kenya National Bureau of Statistics (KNBS).