kenyageo

package module
v0.3.3 Latest Latest
Warning

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

Go to latest
Published: Oct 24, 2026 License: MIT Imports: 15 Imported by: 1

README

kenya-geo

Go Reference CI Go version

Kenya's 47 counties, 290 constituencies, 1,450 wards and 890 post offices for Go, with 2019 census figures and map coordinates. The data is compiled into your binary, so there is nothing to download or configure at runtime.

Features

  • Every county, constituency and ward, with IEBC ward codes and 2022 registered voters
  • 2019 census population by sex, land area and density for each county
  • 2019 census urban and rural population and religious affiliation for each county and for Kenya
  • 2019 census living conditions for each county and for Kenya: households, household size, electricity, piped water, internet use, mobile phone ownership and school attendance
  • ISO 3166-2 codes, former provinces and neighbouring counties
  • Centroids and bounding boxes for counties, centroids for wards
  • Find the ward, constituency and county for any coordinate, or just the county from county boundaries
  • Ward and county boundaries as GeoJSON
  • 890 post offices by postal code, each placed in a county, and the nearest offices to any point
  • A postal address parser that checks the box, postal code and town agree
  • Forgiving name lookups: case, apostrophes, dashes and a trailing "County" don't matter
  • Fuzzy search across counties, constituencies, wards and post offices
  • A JSON HTTP API and a ready-to-run server
  • mapping-kenya, a website built on the package
  • No dependencies outside the standard library; safe for concurrent use

See CHANGELOG.md for what changed in each release.

Install

go get github.com/rmiyoyo/kenya-geo

Requires Go 1.24 or later.

Usage

package main

import (
    "fmt"

    kenyageo "github.com/rmiyoyo/kenya-geo"
)

func main() {
    geo := kenyageo.Default()

    c, ok := geo.CountyByName("murang'a")
    if !ok {
        return
    }
    fmt.Println(c.Code, c.Name, c.Headquarters)

    for _, name := range geo.Constituencies(c.Code) {
        fmt.Println(name, len(geo.WardsInConstituency(name)))
    }
}

This prints 21 Murang'a County Murang'a, then each of Murang'a's constituencies with its number of wards.

Counties
Call Returns
geo.Counties() all 47 counties, ordered by county code
geo.CountyByCode(47) Nairobi County
geo.CountyByName("nairobi") the same county
c.Population, c.Density() 4397073, about 6247 people per km² for Nairobi
c.ISOCode, c.FormerProvince "KE-30", "Nairobi"
geo.NeighbouringCounties(47) Machakos, Kiambu and Kajiado
Constituencies and wards
Call Returns
geo.Constituencies(32) Nakuru's 11 constituency names, alphabetical
geo.WardsInCounty(32) Nakuru's wards as a []Ward
geo.WardsInConstituency("Kibra") Kibra's wards
geo.WardByCode("0040") WAA ward, in Matuga, Kwale
w.Centroid a *LatLng, nil when unknown
w.RegisteredVoters2022 an *int, nil when unknown

Ward names repeat across the country (there are several CENTRAL and TOWNSHIP wards), so store ward codes, not names.

Registered voters

Voter totals add up the 2022 ward figures:

v, ok := geo.CountyVoters(47)
v, ok = geo.ConstituencyVoters("Kibra")
n := geo.NationalVoters()

For Nairobi, CountyVoters(47) gives {RegisteredVoters2022: 2415310, Wards: 85, Complete: true}. NationalVoters adds up all 1,450 wards.

Complete is false when a ward in the total has no figure. Only Della ward (Eldas, Wajir) is missing one, so the Eldas, Wajir and national totals are slightly low. The national total is 22,096,344, a little under IEBC's published 22,120,458, which also counts voters registered in the diaspora and in prisons.

Which ward is this point in?
w, ok := geo.WardAt(-1.2884, 36.8233)
fmt.Println(w.Name, w.Constituency, w.CountyName)

The arguments are latitude then longitude; this point is the KICC, and it prints NAIROBI CENTRAL Starehe Nairobi County. WardAt returns false for points outside Kenya, in the sea, or in one of the few areas without a ward boundary (see the accuracy notes). The boundaries are loaded the first time you call it, which takes about 30 ms; after that a lookup takes a few microseconds.

CountyAt does the same with county boundaries, so it also works where no ward boundary is known:

c, ok := geo.CountyAt(-0.0917, 34.768)

That point is in Kisumu County.

The two use different boundary sets, so within a few kilometres of a county line they can disagree: the ward's CountyName follows the IEBC ward list, while CountyAt follows the county outline.

Boundaries as GeoJSON

WardBoundary and CountyBoundary return the outline as a GeoJSON Feature, ready for Leaflet, Mapbox or QGIS:

f, ok := geo.WardBoundary("1439")
b, _ := json.Marshal(f)

b holds {"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[...]},"properties":{"ward_code":"1439",...}}.

The geometry is always a MultiPolygon with closed rings, outer rings counterclockwise and holes clockwise, as RFC 7946 asks. Properties is the Ward or County, so the JSON carries the same fields as the other lookups. Each call returns a fresh copy, so changing it does not affect later lookups. Wards without a known boundary return false.

Post offices

geo.PostOffices("00100") returns Nairobi GPO, and geo.PostOfficesInCounty(27) returns every post office in Uasin Gishu. A few postal codes are shared by two offices, so PostOffices returns a slice.

for _, p := range geo.PostOfficesNear(-0.2833, 36.0667, 3) {
    fmt.Println(p.Code, p.Name, p.DistanceKm)
}

From central Nakuru this prints 20100 Nakuru 2.7, then Lanet and Kabatini. PostOfficesNear returns the closest post offices to a point, nearest first, with the straight-line distance in kilometres. Only the 577 offices with a known location are considered, so the nearest result can be a few kilometres off where an office without a location is closer. From the KICC, the nearest are City Square and Nairobi GPO.

Postal addresses

ParseAddress reads a Kenyan postal address and checks it against the post office list:

a, err := geo.ParseAddress("P.O. Box 30100, 00100 GPO Nairobi")
fmt.Println(a.Box, a.PostalCode, a.PostOffice.Name)
fmt.Println(a)

This prints 30100 00100 Nairobi Gpo, then the tidied address P.O. Box 30100-00100 Nairobi.

It accepts the usual ways of writing one: P.O. Box, P. O. BOX, PO Box, Box and Private Bag, with the box number and postal code joined by a dash, a comma or a space, in any case, with a name on the lines before and "Kenya" at the end. When the postal code is missing it is taken from the town, so Box 45, Kisumu becomes P.O. Box 45-40100 Kisumu.

The town is checked against the post office for that code. It matches when it is the office's name (one typo allowed) or the county the office is in, so P.O. Box 123-00101 Nairobi is fine even though 00101 is the Jamia office. String prints the address in the standard P.O. Box 123-00100 Nairobi form.

Errors can be checked with errors.Is:

Error Meaning
ErrNoBox no P.O. Box or Private Bag in the text, such as a street address, or a P.O. Box without a number
ErrUnknownPostOffice the postal code doesn't exist, or there is no code and the town isn't a post office
ErrTownMismatch the town doesn't belong to the postal code; the address is still returned, with the office for the code
for _, m := range geo.Search("nakru", 10) {
    fmt.Println(m.Kind, m.Name, m.Score)
}

Results are ranked by how closely they match, and each carries the code of the county it belongs to. A limit of 0 or less returns every match.

Your own data

Load builds the same lookups from any readers, for example a newer copy of the files in data/:

geo, err := kenyageo.Load(countiesFile, wardsFile, postOfficesFile)
err = geo.LoadWardShapes(wardShapesFile)
err = geo.LoadCountyShapes(countyShapesFile)

WardAt and WardBoundary need the ward shapes; CountyAt and CountyBoundary need the county shapes.

Command-line tool

go install github.com/rmiyoyo/kenya-geo/cmd/kenyageo@latest
Command Prints
kenyageo county nakuru neighbours, voters, constituencies and ward counts
kenyageo postcode 30100 the post office for a postal code
kenyageo search kibera fuzzy search results
kenyageo at -1.2884 36.8233 the ward, constituency and county for a point
kenyageo near -0.2833 36.0667 the five closest post offices
kenyageo boundary 1439 a ward or county outline as GeoJSON
kenyageo address "P.O. Box 123-00100 Nairobi" the parsed and checked postal address

Add -json before the command to get JSON instead of text, for scripts and jq:

kenyageo -json county nakuru | jq '.constituencies[] | {name, voters: .voters.registered_voters_2022}'
kenyageo -json at -1.2884 36.8233 | jq -r .ward.name

county gives the county with its voter total and each constituency's; at gives {"ward": ..., "county": ...}, with ward null where no ward boundary is known; the other commands give the same JSON as the HTTP API. boundary always prints GeoJSON.

The exit status is 0 on success, 1 when nothing matches (or an address's town and postal code disagree, in which case the parsed address is still printed) and 2 for a bad command or argument.

HTTP API

The api package serves the same data as JSON, and kenyageo-server runs it. api/README.md is the full reference, with parameters and example responses for every endpoint. The same file is embedded in the package as api.Docs, and mapping-kenya renders it as its API page, so the website's docs always match the version of the API it serves.

go install github.com/rmiyoyo/kenya-geo/cmd/kenyageo-server@latest
kenyageo-server -addr :8080

It listens on $PORT when that is set (as Cloud Run, Fly.io and Heroku set it), and otherwise on :8080; -addr overrides both. On SIGTERM or Ctrl-C it stops taking new connections and finishes the requests in flight before exiting.

Docker
docker build -t kenyageo-server .
docker run -p 8080:8080 kenyageo-server

The image is about 18 MB: a static binary on the distroless base, running as a non-root user, with no shell or package manager. Its health check runs kenyageo-server -healthcheck, which asks the running server for a county and exits non-zero if it doesn't answer. CI builds the image and checks a running container on every pull request.

Endpoint Returns
GET /counties all 47 counties
GET /counties/{county} one county, by code (32) or name (nakuru)
GET /counties/{county}/constituencies constituency names
GET /counties/{county}/neighbours counties that share a border with it
GET /counties/{county}/boundary the county outline as a GeoJSON Feature
GET /counties/{county}/voters registered voters, ward count and whether every ward has a figure
GET /counties/{county}/wards wards in the county
GET /counties/{county}/postoffices post offices in the county
GET /constituencies/{name}/wards wards in a constituency, e.g. /constituencies/kibra/wards
GET /constituencies/{name}/voters registered voters in a constituency
GET /voters the national total
GET /national Kenya as a whole: 2019 census population, urban and rural, living conditions and religion, plus 2022 voters
GET /wards/{code} one ward
GET /wards/{code}/boundary the ward outline as a GeoJSON Feature
GET /postcodes/{code} post offices with that postal code
GET /search?q=nakru&limit=10 fuzzy search results with kind, name, code, county_code and score
GET /addresses?q=P.O.%20Box%20123-00100%20Nairobi the parsed address with its post office; 400 if there is no box, 404 for an unknown office, 422 if the town and code disagree
GET /at?lat=-1.2884&lng=36.8233 the ward at that point
GET /counties/at?lat=-0.0917&lng=34.768 the county at that point
GET /postoffices/near?lat=-0.2833&lng=36.0667&limit=5 the closest post offices, nearest first, with distance_km
GET /openapi.json the OpenAPI description of this API

GET /openapi.json returns an OpenAPI 3.1 description of every endpoint, with the shape of each response. Load it into Swagger UI, Postman or a client generator such as oapi-codegen. The file lives at api/openapi.json; tests check that it lists exactly the routes the server has and that real responses match its schemas, so it can't quietly drift out of date.

Boundary responses use the application/geo+json content type. Errors come back as {"error": "..."} with status 400 for a bad query and 404 when nothing matches. Responses allow requests from any origin, so a web page can call the API directly.

To add the endpoints to your own server, mount the handler:

mux.Handle("/geo/", http.StripPrefix("/geo", api.New(kenyageo.Default())))

Data

File Contents
data/counties.json code, name, headquarters, ISO code, former province, population by sex, area, centroid, bounding box, neighbouring county codes, 2019 census urban and rural population, living conditions and religious affiliation
data/kenya.json urban and rural population, living conditions and religious affiliation for Kenya as a whole, from Data.National
data/wards.json ward code, name, constituency, county, 2022 registered voters, centroid
data/postoffices.json postal code, name, county, how the county was decided, location
data/wardshapes.json ward code and boundary polygons, as GeoJSON-style [lng, lat] rings
data/countyshapes.json county code and boundary polygons, in the same format

The same JSON files can be used from any language.

Accuracy notes
  • Neighbouring counties are worked out from the county boundaries: two counties are neighbours when their outlines run within about a kilometre of each other for at least three boundary points. Borders across Lake Victoria count, so Siaya and Homa Bay are neighbours.
  • Ward centroids and boundaries are known for 1,437 of the 1,450 wards. The other 13 have names too different from the boundary data to match safely, so WardAt finds nothing in those areas.
  • Boundaries are simplified, so a point within a few hundred metres of a ward line can land in the neighbouring ward. A handful of wards on county borders have centroids that fall just across the line.
  • Post office locations are given for 577 of the 890 offices. GeoNames places 553 of them precisely; the other 24 come from OpenStreetMap. location_source says which: geonames, openstreetmap, or empty when the location is unknown. county_source says how each office's county was decided: geonames, location, manual or neighbours.
  • An OpenStreetMap post office is used only when it matches exactly one office without a location: by postal code and name, by the same name, or by a name that is part of the office's name (Hardy for Langata Hardy). A match is skipped when the point lies in a different county, when another office could claim it, or when a name-only match sits within 300 m of an office GeoNames already places. That leaves out, for example, Kamiti and Kenyatta University, which OpenStreetMap places just inside Nairobi while GeoNames files them under Kiambu.
  • Living conditions are copied from the KNBS 2019 census tables, not estimated. Average household size is worked out from the Volume I population in households and number of households, because that table's own column has lost its decimal point (39 for 3.9). Piped water counts water piped into the dwelling or to the yard or plot; public taps are left out. School attendance adds the 6–13 and 14–17 age groups; Kenya's figure is worked out from the county totals, since that table has no national row. The tests check the national figures against KNBS's published 50.4 % electricity, 22.6 % internet use and 47.3 % mobile phone ownership, and that the county households add up to the national 12,143,913.
  • The source data had several errors, such as wards filed under the wrong constituency, mixed census years and a malformed ward code. They are corrected when the data is generated; see internal/gendata/fixes.go.

Sources

Source Used for Licence
KNBS 2019 Kenya Population and Housing Census, Volumes I, II and IV county population by sex, land area, households and living conditions CC BY
geoBoundaries gbOpen KEN ADM1/ADM2/ADM3 ISO codes, centroids, bounding boxes, ward boundaries public domain
GeoNames postal codes post offices CC BY 4.0
OpenStreetMap via Overpass API post office locations GeoNames lacks ODbL 1.0
IEBC county and ward lists codes, names, headquarters, wards, 2022 voters public record

Post office data is © GeoNames, used under CC BY 4.0. Some post office locations are © OpenStreetMap contributors, available under the Open Database License.

data/source/osm-postoffices.json is a saved Overpass API result, so generating the data doesn't depend on Overpass being up. To refresh it, run this and then regenerate:

curl -sS https://overpass-api.de/api/interpreter --data-urlencode 'data=[out:json][timeout:180];area["ISO3166-1"="KE"][admin_level=2]->.ke;(node["amenity"="post_office"](area.ke);way["amenity"="post_office"](area.ke);relation["amenity"="post_office"](area.ke););out center tags;' -o data/source/osm-postoffices.json

Contributing

Issues and pull requests are welcome. To regenerate the data after changing a source or a fix, run the generator and then the tests. The generator downloads the raw sources, applies the fixes and writes data/*.json.

go run ./internal/gendata
go test ./...

Don't edit data/*.json by hand. Change the sources in data/source/ or the fixes in internal/gendata/ instead.

CI runs these on every pull request, so run them before pushing. gofmt -l . should print nothing.

gofmt -l .
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@2025.1.1 ./...
go test -race ./...

CI also runs each fuzz test for 15 seconds. They throw random input at normalize, ParseAddress and Search and check properties that must always hold: normalizing twice gives the same result, a parsed address prints back to the same address, search results are sorted and scored between 0 and 1. To fuzz one for longer:

go test -run '^$' -fuzz FuzzParseAddress -fuzztime 5m .

A failing input is saved under testdata/fuzz/ and from then on runs as an ordinary test case, so commit it with the fix.

Pull requests that touch internal/gendata/ or data/ also regenerate the data from scratch and fail if the committed data/*.json doesn't match. The geoBoundaries files are downloaded from the latest release, so that check can also fail when geoBoundaries changes upstream; regenerating and committing the result fixes it.

Releasing

Releases are Git tags such as v0.3.0. The Go module proxy and pkg.go.dev pick a tag up on their own; nothing is uploaded anywhere.

  1. Make sure main has everything for the release and CI is green on it.

  2. In a pull request, change the top of CHANGELOG.md from "not yet tagged" to the release date, and check "version" in api/openapi.json and the version named at the top of api/README.md match the new tag. Merge it.

  3. Tag the merge commit on main and push the tag:

    git checkout main
    git pull
    git tag -a v0.3.0 -m "v0.3.0"
    git push origin v0.3.0
    
  4. On GitHub, open Releases → Draft a new release, pick the tag, and paste that version's section of the changelog as the notes.

  5. Ask the module proxy for the new version, so go get and pkg.go.dev see it straight away:

    GOPROXY=https://proxy.golang.org go list -m github.com/rmiyoyo/kenya-geo@v0.3.0
    
  6. Move projects that use the package onto the tag, for example mapping-kenya:

    go get github.com/rmiyoyo/kenya-geo@v0.3.0
    go mod tidy
    

Pick the number by what changed since the last tag: a new minor version (v0.4.0) for new features, a patch (v0.3.1) for fixes only. Until 1.0, a change that breaks existing code also goes in a minor version, and the changelog says so under Breaking.

Documentation

Overview

Package kenyageo holds Kenya's 47 counties, their constituencies and 1,450 wards, and 890 post offices, with 2019 census figures, IEBC registered voters and boundaries, all embedded in the package.

Start with Default, which loads the embedded data once and is safe for concurrent use:

geo := kenyageo.Default()
nakuru, _ := geo.CountyByName("Nakuru")
ward, _ := geo.WardAt(-1.2884, 36.8233)
Example
package main

import (
	"fmt"

	kenyageo "github.com/rmiyoyo/kenya-geo"
)

func main() {
	geo := kenyageo.Default()
	nakuru, _ := geo.CountyByName("nakuru")
	fmt.Println(nakuru.Code, nakuru.Name, nakuru.Population)
}
Output:
32 Nakuru County 2162202

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrNoBox             = errors.New("no P.O. Box or Private Bag found")
	ErrUnknownPostOffice = errors.New("unknown post office")
	ErrTownMismatch      = errors.New("town does not match the postal code")
)

Errors returned by Data.ParseAddress. Test for them with errors.Is.

Functions

This section is empty.

Types

type Address added in v0.2.0

type Address struct {
	Box        string      `json:"box,omitempty"`
	PrivateBag bool        `json:"private_bag"`
	PostalCode string      `json:"postal_code"`
	Town       string      `json:"town,omitempty"`
	PostOffice *PostOffice `json:"post_office"`
}

Address is a parsed Kenyan postal address such as "P.O. Box 123-00100 Nairobi".

func (Address) String added in v0.2.0

func (a Address) String() string

String writes the address in the standard form, such as "P.O. Box 123-00100 Nairobi".

type County

type County struct {
	Code           int    `json:"code"`
	Name           string `json:"name"`
	ISOCode        string `json:"iso_code"`
	FormerProvince string `json:"former_province"`
	Headquarters   string `json:"headquarters"`
	// HeadquartersAliases lists other names in use for the headquarters
	// town, such as Vihiga for Mbale.
	HeadquartersAliases []string `json:"headquarters_aliases,omitempty"`

	Population         int     `json:"population"`
	PopulationMale     int     `json:"population_male"`
	PopulationFemale   int     `json:"population_female"`
	PopulationIntersex int     `json:"population_intersex"`
	PopulationUrban    int     `json:"population_urban"`
	PopulationRural    int     `json:"population_rural"`
	AreaKm2            float64 `json:"area_km2"`

	Centroid   LatLng     `json:"centroid"`
	BBox       [4]float64 `json:"bbox"`
	Neighbours []int      `json:"neighbours"`

	Living   Living   `json:"living"`
	Religion Religion `json:"religion"`
}

County is one of Kenya's 47 counties, with its 2019 census figures, location and neighbours.

func (County) Density

func (c County) Density() float64

Density returns people per square kilometre.

func (County) UrbanPct added in v0.3.0

func (c County) UrbanPct() float64

UrbanPct returns the percentage of the county's people who live in urban areas.

Example
package main

import (
	"fmt"

	kenyageo "github.com/rmiyoyo/kenya-geo"
)

func main() {
	geo := kenyageo.Default()
	kilifi, _ := geo.CountyByCode(3)
	fmt.Printf("%.1f%% urban, %.1f%% use the internet\n", kilifi.UrbanPct(), kilifi.Living.InternetPct)
}
Output:
27.1% urban, 15.1% use the internet

type Data

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

Data holds the counties, wards and post offices and answers questions about them. Use Default for the embedded data, or Load for your own. A Data is safe for concurrent use once loaded.

func Default

func Default() *Data

Default returns the data embedded in the package, including boundaries and national figures. It is loaded on first use and shared afterwards.

func Load

func Load(counties, wards, postOffices io.Reader) (*Data, error)

Load reads counties, wards and post offices in the JSON format of the files in the data directory. Boundaries are not loaded; add them with Data.LoadWardShapes and Data.LoadCountyShapes.

func (*Data) Constituencies

func (d *Data) Constituencies(countyCode int) []string

Constituencies returns the names of a county's constituencies, sorted.

func (*Data) ConstituencyVoters added in v0.2.0

func (d *Data) ConstituencyVoters(name string) (VoterTotal, bool)

ConstituencyVoters returns the registered voters in a constituency's wards, matching the name like Data.CountyByName.

func (*Data) Counties

func (d *Data) Counties() []County

Counties returns all 47 counties in code order.

func (*Data) CountyAt added in v0.2.0

func (d *Data) CountyAt(lat, lng float64) (County, bool)

CountyAt returns the county containing a point. Boundaries are simplified, so treat points near a county line as approximate.

Example
c, ok := Default().CountyAt(-0.0917, 34.768)
fmt.Println(c.Code, c.Name, ok)
Output:
42 Kisumu County true
Example
package main

import (
	"fmt"

	kenyageo "github.com/rmiyoyo/kenya-geo"
)

func main() {
	geo := kenyageo.Default()
	county, _ := geo.CountyAt(-0.0917, 34.768)
	fmt.Println(county.Name)
}
Output:
Kisumu County

func (*Data) CountyBoundary added in v0.2.0

func (d *Data) CountyBoundary(code int) (Feature[County], bool)

CountyBoundary returns a county's outline as a GeoJSON Feature.

Example
f, _ := Default().CountyBoundary(47)
b, _ := json.Marshal(f)
fmt.Println(f.Properties.Name, f.Geometry.Type, len(b) > 1000)
Output:
Nairobi County MultiPolygon true

func (*Data) CountyByCode

func (d *Data) CountyByCode(code int) (County, bool)

CountyByCode returns the county with a code from 1 to 47.

func (*Data) CountyByName

func (d *Data) CountyByName(name string) (County, bool)

CountyByName returns the county with a name. Case, apostrophes, dashes and a trailing "County" don't matter, so "nakuru" and "Murang'a County" both work.

Example
c, ok := Default().CountyByName("taita taveta")
fmt.Println(c.Code, c.Name, c.Headquarters, ok)
Output:
6 Taita-Taveta County Mwatate true

func (*Data) CountyVoters added in v0.2.0

func (d *Data) CountyVoters(code int) (VoterTotal, bool)

CountyVoters returns the registered voters in a county's wards.

Example
package main

import (
	"fmt"

	kenyageo "github.com/rmiyoyo/kenya-geo"
)

func main() {
	geo := kenyageo.Default()
	v, _ := geo.CountyVoters(47)
	fmt.Println(v.RegisteredVoters2022, v.Wards, v.Complete)
}
Output:
2415310 85 true
Example
v, _ := Default().CountyVoters(47)
fmt.Println(v.RegisteredVoters2022, v.Wards, v.Complete)
Output:
2415310 85 true

func (*Data) LoadCountyShapes added in v0.2.0

func (d *Data) LoadCountyShapes(r io.Reader) error

LoadCountyShapes reads county boundaries in the format of data/countyshapes.json, for data from Load.

func (*Data) LoadWardShapes added in v0.2.0

func (d *Data) LoadWardShapes(r io.Reader) error

LoadWardShapes reads ward boundaries in the format of data/wardshapes.json, for data from Load.

func (*Data) National added in v0.3.0

func (d *Data) National() (National, bool)

National returns the census figures for Kenya as a whole. It reports false for data from Load, which has no national figures.

Example
package main

import (
	"fmt"

	kenyageo "github.com/rmiyoyo/kenya-geo"
)

func main() {
	geo := kenyageo.Default()
	kenya, _ := geo.National()
	fmt.Printf("%.1f%% Christian, %.1f%% of homes on mains electricity\n",
		kenya.Religion.Pct(kenya.Religion.Christian()), kenya.Living.ElectricityPct)
}
Output:
85.5% Christian, 50.4% of homes on mains electricity

func (*Data) NationalLiving added in v0.3.0

func (d *Data) NationalLiving() (Living, bool)

NationalLiving returns Kenya's living conditions, like Data.National.

func (*Data) NationalVoters added in v0.2.0

func (d *Data) NationalVoters() VoterTotal

NationalVoters returns the registered voters in every ward.

func (*Data) NeighbouringCounties added in v0.2.0

func (d *Data) NeighbouringCounties(code int) []County

NeighbouringCounties returns the counties that share a border with the county with code.

Example
for _, c := range Default().NeighbouringCounties(47) {
	fmt.Println(c.Name)
}
Output:
Machakos County
Kiambu County
Kajiado County

func (*Data) ParseAddress added in v0.2.0

func (d *Data) ParseAddress(s string) (Address, error)

ParseAddress parses a postal address and finds its post office. It accepts "P.O. Box", "Box" and "Private Bag", with the postal code joined to the box by a dash or written separately. When the town and postal code belong to different offices it returns the parsed address with ErrTownMismatch.

Example
package main

import (
	"errors"
	"fmt"

	kenyageo "github.com/rmiyoyo/kenya-geo"
)

func main() {
	geo := kenyageo.Default()
	a, err := geo.ParseAddress("p.o. box 123, nairobi 00100")
	fmt.Println(a, a.PostOffice.Name, err)

	_, err = geo.ParseAddress("P.O. Box 90-40100 Eldoret")
	fmt.Println(errors.Is(err, kenyageo.ErrTownMismatch))
}
Output:
P.O. Box 123-00100 Nairobi Nairobi Gpo <nil>
true
Example
a, err := Default().ParseAddress("P.O. Box 30100, 00100 GPO Nairobi")
fmt.Println(a.Box, a.PostalCode, a.PostOffice.Name, err)
fmt.Println(a)
Output:
30100 00100 Nairobi Gpo <nil>
P.O. Box 30100-00100 Nairobi

func (*Data) PostOffices

func (d *Data) PostOffices(postalCode string) []PostOffice

PostOffices returns the post offices with a postal code. Several offices can share one code.

func (*Data) PostOfficesInCounty

func (d *Data) PostOfficesInCounty(countyCode int) []PostOffice

PostOfficesInCounty returns the post offices in a county, by postal code.

func (*Data) PostOfficesNear added in v0.2.0

func (d *Data) PostOfficesNear(lat, lng float64, n int) []NearbyPostOffice

PostOfficesNear returns up to n post offices closest to a point, nearest first, by great-circle distance. Offices with no known location are skipped.

Example
package main

import (
	"fmt"

	kenyageo "github.com/rmiyoyo/kenya-geo"
)

func main() {
	geo := kenyageo.Default()
	for _, p := range geo.PostOfficesNear(-0.2833, 36.0667, 2) {
		fmt.Printf("%s %s %.1f km\n", p.Code, p.Name, p.DistanceKm)
	}
}
Output:
20100 Nakuru 2.7 km
20112 Lanet 8.2 km
Example
for _, p := range Default().PostOfficesNear(-0.2833, 36.0667, 2) {
	fmt.Printf("%s %s %.1f km\n", p.Code, p.Name, p.DistanceKm)
}
Output:
20100 Nakuru 2.7 km
20112 Lanet 8.2 km

func (*Data) Search

func (d *Data) Search(query string, limit int) []Match

Search finds counties, constituencies, wards and post offices whose names match query, allowing for typos, best first. A limit of 0 or less returns every match.

Example
package main

import (
	"fmt"

	kenyageo "github.com/rmiyoyo/kenya-geo"
)

func main() {
	geo := kenyageo.Default()
	for _, m := range geo.Search("nakru", 3) {
		fmt.Println(m.Kind, m.Name, m.Code)
	}
}
Output:
county Nakuru County 32
post office Nakuru 20100
Example
for _, m := range Default().Search("makadara", 3) {
	fmt.Printf("%s %s\n", m.Kind, m.Name)
}
Output:
constituency Makadara
post office Makadara
ward MJI WA KALE/MAKADARA

func (*Data) WardAt added in v0.2.0

func (d *Data) WardAt(lat, lng float64) (Ward, bool)

WardAt returns the ward containing a point. It reports false outside Kenya and in the few areas with no known ward boundary.

Example
package main

import (
	"fmt"

	kenyageo "github.com/rmiyoyo/kenya-geo"
)

func main() {
	geo := kenyageo.Default()
	ward, ok := geo.WardAt(-1.2884, 36.8233)
	fmt.Println(ok, ward.Name, ward.Constituency, ward.CountyName)
}
Output:
true NAIROBI CENTRAL Starehe Nairobi County
Example
w, ok := Default().WardAt(-1.2884, 36.8233)
fmt.Println(w.Code, w.Name, w.Constituency, w.CountyName, ok)
Output:
1439 NAIROBI CENTRAL Starehe Nairobi County true

func (*Data) WardBoundary added in v0.2.0

func (d *Data) WardBoundary(code string) (Feature[Ward], bool)

WardBoundary returns a ward's outline as a GeoJSON Feature. It reports false for the few wards with no known boundary.

func (*Data) WardByCode

func (d *Data) WardByCode(code string) (Ward, bool)

WardByCode returns the ward with a four-digit IEBC code such as "0040".

func (*Data) WardsInConstituency

func (d *Data) WardsInConstituency(name string) []Ward

WardsInConstituency returns the wards in the constituency with a name, matched like Data.CountyByName.

func (*Data) WardsInCounty

func (d *Data) WardsInCounty(countyCode int) []Ward

WardsInCounty returns the wards in a county, by ward code.

type Feature added in v0.2.0

type Feature[P any] struct {
	Type       string   `json:"type"`
	Geometry   Geometry `json:"geometry"`
	Properties P        `json:"properties"`
}

Feature is a GeoJSON Feature with a MultiPolygon geometry and P as its properties.

type Geometry added in v0.2.0

type Geometry struct {
	Type        string           `json:"type"`
	Coordinates [][][][2]float64 `json:"coordinates"`
}

Geometry is a GeoJSON MultiPolygon: polygons made of rings of [lng, lat] points.

type Kind

type Kind int

Kind is the kind of place a search Match found.

const (
	KindCounty Kind = iota
	KindConstituency
	KindWard
	KindPostOffice
)

The kinds of place Data.Search can find.

func (Kind) String

func (k Kind) String() string

String returns "county", "constituency", "ward" or "post office".

type LatLng

type LatLng struct {
	Lat float64 `json:"lat"`
	Lng float64 `json:"lng"`
}

LatLng is a point in WGS 84 decimal degrees.

type Living added in v0.3.0

type Living struct {
	Households          int     `json:"households"`
	HouseholdSize       float64 `json:"average_household_size"`
	ElectricityPct      float64 `json:"electricity_pct"`
	PipedWaterPct       float64 `json:"piped_water_pct"`
	InternetPct         float64 `json:"internet_pct"`
	MobilePhonePct      float64 `json:"mobile_phone_pct"`
	SchoolAttendancePct float64 `json:"school_attendance_pct"`
}

Living holds living conditions from the 2019 census for a county or for Kenya. The fields ending in Pct are percentages from 0 to 100.

type Match

type Match struct {
	Kind       Kind
	Name       string
	Code       string
	CountyCode int
	Score      float64
}

Match is one search result. Code is the county code, ward code or postal code, and is empty for constituencies. Score runs from 0 to 1, where 1 is an exact match.

type National added in v0.3.0

type National struct {
	PopulationUrban int      `json:"population_urban"`
	PopulationRural int      `json:"population_rural"`
	Living          Living   `json:"living"`
	Religion        Religion `json:"religion"`
}

National holds the 2019 census figures for Kenya as a whole.

type NearbyPostOffice added in v0.2.0

type NearbyPostOffice struct {
	PostOffice
	DistanceKm float64 `json:"distance_km"`
}

NearbyPostOffice is a post office and its distance from a point.

type PostOffice

type PostOffice struct {
	Code           string  `json:"postal_code"`
	Name           string  `json:"name"`
	CountyCode     int     `json:"county_code"`
	CountySource   string  `json:"county_source"`
	Location       *LatLng `json:"location"`
	LocationSource string  `json:"location_source"`
}

PostOffice is a post office and the postal code it serves.

type Religion added in v0.3.0

type Religion struct {
	Total             int `json:"total"`
	Catholic          int `json:"catholic"`
	Protestant        int `json:"protestant"`
	Evangelical       int `json:"evangelical"`
	AfricanInstituted int `json:"african_instituted"`
	Orthodox          int `json:"orthodox"`
	OtherChristian    int `json:"other_christian"`
	Islam             int `json:"islam"`
	Hindu             int `json:"hindu"`
	Traditionist      int `json:"traditionist"`
	OtherReligion     int `json:"other_religion"`
	NoReligion        int `json:"no_religion"`
	DontKnow          int `json:"dont_know"`
	NotStated         int `json:"not_stated"`
}

Religion counts people in households by religious affiliation in the 2019 census. The groups add up to Total.

func (Religion) Christian added in v0.3.0

func (r Religion) Christian() int

Christian returns the number of people in all the Christian groups.

func (Religion) Pct added in v0.3.0

func (r Religion) Pct(n int) float64

Pct returns n as a percentage of Total, or 0 when Total is 0.

type VoterTotal added in v0.2.0

type VoterTotal struct {
	RegisteredVoters2022 int  `json:"registered_voters_2022"`
	Wards                int  `json:"wards"`
	Complete             bool `json:"complete"`
}

VoterTotal adds up the 2022 registered voters in a group of wards. Complete is false when one or more wards have no published figure, so the total is a lower bound.

type Ward

type Ward struct {
	Code         string `json:"ward_code"`
	Name         string `json:"name"`
	Constituency string `json:"constituency_name"`
	CountyCode   int    `json:"county_code"`
	CountyName   string `json:"county_name"`

	RegisteredVoters2022 *int `json:"registered_voters_2022"`

	Centroid *LatLng `json:"centroid"`
}

Ward is an IEBC electoral ward, with its constituency, county and 2022 registered voters.

Directories

Path Synopsis
Package api serves the kenyageo data as a JSON HTTP API.
Package api serves the kenyageo data as a JSON HTTP API.
cmd
kenyageo command
Kenyageo looks up Kenya's counties, wards, post offices and addresses from the command line.
Kenyageo looks up Kenya's counties, wards, post offices and addresses from the command line.
kenyageo-server command
Kenyageo-server serves the kenyageo JSON API over HTTP.
Kenyageo-server serves the kenyageo JSON API over HTTP.
internal
gendata command

Jump to

Keyboard shortcuts

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