Geography and Boundary Changes
Versioned geography in MDS: change events (split, merge, rename, recode, reparent, boundary change) with worked examples, future-effective changes, the crosswalk, per-version boundaries in MinIO, and
Administrative geography changes more often than it seems. Countries split districts to bring services closer, merge small ones, rename them, move them from one province to another and redraw their boundaries. Each such change touches data that other systems hold: a registry records a person's woreda; a report counts farmers per zone; a dashboard sums area per region; a map draws the shapes.
This page explains how MDS versions the geography, how each kind of change is recorded, and what consumers should do when a new geography version is published.
The examples use Ethiopian P-codes (region ET04 → zone ET0412 → woreda ET041203). Kebele codes (one level below woreda) are illustrative: the ETH country pack stops at woreda.
Geography is versioned as one dataset
Datasets are versioned one at a time. Geography is versioned as a whole: its levels, all its units and their boundaries together form one geography version.
The reason is that a geographic change ripples across levels. Splitting a woreda creates new units at woreda level, changes the set of children of its zone, changes the zone's boundary if the split moved a line, and may move kebeles to a new parent. If each level were versioned separately, a consumer could read zones at one version and woredas at another and get a hierarchy that never existed. One version number for the whole geography means every read sees a consistent hierarchy and a matching set of boundaries.
A geography version has the same lifecycle as a dataset version (DRAFT → SUBMITTED → PUBLISHED, or REJECTED or DISCARDED; see Concepts), the same never-reused version numbers, the same effective_from, and the same latest / pinned / as-of reads. In addition it carries:
its levels (
level_id,level_mnemonic, parent level,display,display_i18n);its units:
unit_id(the P-code), level,name,name_i18n, parent,status(ACTIVEorRETIRED),valid_fromandvalid_to(set on publish to the version'seffective_fromfor units it creates or retires);its change events: what happened between its base version and this one;
its boundary objects: the MinIO key of each level's GeoJSON (
boundary_objects, keyed by level mnemonic);its
countryandowner_org.
Units are never deleted
As with dataset entries, a unit is never deleted. When a woreda ceases to exist, the next geography version keeps it with status RETIRED and a valid_to date. So:
a record holding the old P-code still resolves to a name, a level and a parent;
the old unit's boundary is still in the old version's boundary files;
the crosswalk can say what replaced it.
Reads leave retired units out unless include_retired: true is passed. Retiring a unit that has active descendants fails unless cascade: true is passed. A unit added in the current draft and never published is simply removed.
Change events
Every geography draft records why units changed, as change events. These are the lineage: the link from old units to new ones. Without them, a consumer would see only that ET041203 is gone and two new codes appeared, with no way to know they are related.
Change type
What happened
from_units
to_units
Effect on units in the new version
CREATE
New units that did not come from existing ones
(none)
the new units
New units ACTIVE
RETIRE
Units cease to exist and nothing replaces them
the units
(none)
Units RETIRED
RENAME
Same unit, same code, new name
one unit
the same unit
Name changed
RECODE
Same unit, new P-code, same level
the old code
the new code
Old code RETIRED, new code ACTIVE
SPLIT
One unit becomes two or more, at the same level
the old unit
the new units
Old unit RETIRED (or kept, if it continues as one of the parts); new units ACTIVE
MERGE
Two or more units become one, at the same level
the old units
the new (or surviving) unit
Old units RETIRED (except a surviving one); new unit ACTIVE
REPARENT
A unit moves to a different parent
one unit
the same unit
Parent changed
BOUNDARY_CHANGE
A boundary line moves; no unit is created or removed
the affected units
the same units
Units unchanged; boundary files change
Each event also has an effective_date (by default the version's effective date) and a note (for example the proclamation or decision that made the change).
Edit the units first, then record the event. record_geo_change checks the event against the draft's base version and the draft: a CREATE's units must be active in the draft and new; a RETIRE's must be active in the base and retired in the draft; a RENAME or REPARENT names one unit whose name or parent actually differs; a RECODE's old code must be retired and its new code new; the parts of a SPLIT or MERGE must be active in the draft and the old units retired (or be the surviving unit). A unit can end in only one RETIRE, SPLIT, MERGE or RECODE per version. The first geography version has no base and so no events. A wrong event can be removed with delete_geo_change; all events are validated again on submit.
Unexplained changes are not blocked: the lineage is completed on submit. For every unit that was created, retired, renamed or moved in the draft without an event naming it, MDS adds an automatic CREATE, RETIRE, RENAME or REPARENT event, and for every unit whose geometry changed in an uploaded boundary file without another event covering it, a BOUNDARY_CHANGE. Automatic events are flagged is_auto: true and noted (auto: unit created, auto: parent ET0412 -> ET0415, …). So the crosswalk can always follow a unit, but an automatic RETIRE plus CREATE says nothing about a split or merge: record those explicitly.
Example 1: a woreda is split into two
Woreda ET041203 (Adaa) in zone ET0412 is split into two new woredas, Adaa North and Adaa South, from 1 January 2027.
In a geography draft (based on the current version, say version 2):
Add units
ET041217(Adaa North) andET041218(Adaa South), parentET0412.Move each of the old woreda's kebeles to the new woreda it now belongs to (set its new parent). MDS adds a
REPARENTevent for each on submit, or record them yourself (one event per kebele).Retire
ET041203(it has no active children left).Record the change event:
Upload the new woreda-level boundary file (and the kebele level's, if kebele shapes changed) with
upload_draft_boundary.Set the version's
effective_fromto2027-01-01and submit.
Once approved, geography version 3 is published but is not latest until 1 January 2027. Until then, latest still returns version 2 with ET041203 active.
Example 2: two kebeles are merged
Kebeles ET04120305 and ET04120306 are merged into one. The country decides the merged kebele keeps the code ET04120305.
ET04120306 is retired; ET04120305 stays active with a larger boundary. Had the country given the merged kebele a new code, both old codes would be retired and the new one created.
Example 3: a zone is renamed
Zone ET0412 is renamed. Its code does not change, so no record holding it is affected.
Only the unit's name (and name_i18n) changes in the new version. Reading the unit at an older version still returns the old name, which is what a historical report should print.
Example 4: a woreda is moved to another zone
Woreda ET041205 moves from zone ET0412 to zone ET0415.
Because P-codes nest (a child's code begins with its parent's), moving a unit breaks the nesting unless the unit is given a new code. A country can do either:
Keep the code. Change the woreda's parent to
ET0415and record aREPARENT(or let MDS add it on submit). The woreda keepsET041205. Records are unaffected, but the code no longer tells you the parent: consumers must read the parent from MDS rather than from the code's prefix.Recode. Add
ET041509underET0415, retireET041205and record aRECODEfrom one to the other. Nesting holds, but records that hold the old code now point at a retired unit, and must be mapped through the crosswalk. (AREPARENTis not recorded here: the old unit no longer exists in the new version, and the new one has no previous parent.)
A REPARENT event names only the unit; the old and new parents are the unit's parent in the base version and in the new one (an automatic event also puts them in its note).
In every case, a zone or region whose area changed also needs its boundary updated; a pure boundary adjustment with no other change is a BOUNDARY_CHANGE.
Future changes and effective dates
Administrative changes are usually decided before they take effect. MDS lets the whole change be prepared, approved and published ahead of time:
The geography version's
effective_fromsays when it becomeslatest. Before that date,latestreturns the previous version; the new version can be read by number to prepare (for example, to train staff or pre-build a map).Each change event has its own
effective_date, and each unit hasvalid_fromandvalid_to, so a consumer can see when a unit came into or went out of existence.An
as_ofread returns the version in effect on a given date, which is how a report for "farmers per woreda on 30 June 2026" gets the woredas as they were on that day.
The current-state tables (g2p_geo_levels, g2p_geo_level_values) switch to a future-effective version when its effective_from passes: MDS checks on start, on every publish and every catalogue.effectiveRefreshSeconds (300 s by default), refreshes the tables and writes a geo.version.effective event to the change log. That event is delivered like the others: to the Audit Manager and, with a WebSub hub, on the <prefix>.geo.effective topic. So a consumer is told both when the change is published (to prepare) and when it takes effect (to switch); see For consumers.
The crosswalk
The crosswalk answers: "This record holds unit X from geography version A. What is it in version B?" It follows the change events between the two versions, across every version in between, and returns the successor units.
units are the successors as they are in to_version; unmapped lists units that end without a successor (retired with nothing replacing them); path lists the events followed; unchanged is true when the unit maps to itself with no event on the way. to_version defaults to latest.
How to use the answer depends on the change:
RENAME, REPARENT, BOUNDARY_CHANGE
The same unit
Nothing to map; the code still holds
RECODE, MERGE
Exactly one
Map automatically
SPLIT
Several
Cannot be mapped automatically. The record's location (GPS, kebele, address) decides which part it falls in; otherwise it must be reviewed
RETIRE
None (in unmapped)
Flag for review
The crosswalk works in both directions: from an old version to a newer one (to report old records against current geography) and from a newer version back to an older one (direction: "backward", to compare with a historical baseline). Going backward it returns predecessors, and unmapped lists units that were created without a predecessor.
Boundaries in MinIO, per version
Boundaries remain GeoJSON files in MinIO (bucket openg2p-geo), as described in Country Data Architecture. What changes is that they are stored per geography version, under immutable keys:
A boundary file (a GeoJSON
FeatureCollectionfor one level) is uploaded to a draft, under the draft's version key, withupload_draft_boundary. It can be replaced while the version is a draft. Features are matched to units byproperties.pcode(orunit_id,level_value_id, or the featureid), so MDS can tell which units' geometry changed.Version numbers are never reused (a discarded draft keeps its number), so a key belongs to exactly one version and a published version's objects are never overwritten. A later change uploads new files under the next version's key. As a safeguard for older data, an upload is refused (
G2P-CAT-409) if any other version, whatever its status, already records the key; the country-pack loader applies the same check.A level whose file did not change is not copied: the new version's
boundary_objectspoints at the earlier version's object. In the example above, version 2 changed no woreda shapes, so itsworedaentry is stillgeo/ETH/v1/woreda.geojson.get_geo_boundaryreturns a level's object key for a version, with apresigned_urland, when a public base URL is configured, a plainurl; withstream: trueit returns the GeoJSON itself.Uploading needs the boundary store configured (chart
masterDataAPI.catalogue.boundaryStore). Objects uploaded to a draft that is then discarded stay in the bucket, recorded on theDISCARDEDversion; since its number is never used again, no later version writes to those keys.
So a map built for version 2 and a map built for version 3 can sit side by side, and a historical map always has its own shapes.
When a new geography version is published
Publishing a new geography version does not change any data a consumer holds. What each kind of consumer should do:
Registries (records holding a unit code)
Keep the recorded code. It was true when recorded, and it still resolves (retired units are kept). Store the geography version with the record if you can (see For consumers). Do not rewrite records automatically
Data entry
New records use latest, so new woredas appear in dropdowns from the effective date and retired ones disappear. Screens showing an existing record read with include_retired: true
Current reporting
Map old codes to current units through the crosswalk at query or extract time. One-to-one changes map automatically; splits need the record's location or a review queue
Aggregates and dashboards
Recompute aggregates keyed by unit (counts per woreda, area per zone) against the new version, from the underlying records. An aggregate kept per unit code must not mix versions
Historical reports
Read the geography as of the report's date, or pinned to the version the report was produced with, so the report can be reproduced
Maps
Rebuild using the new version's boundary files; keep the old version's build for historical views
Programmes and anything pinned
Re-pin deliberately. A programme that targets woredas at version 2 stays on version 2 until someone reviews its targeting against version 3 and moves it
Consumers learn of a new version from the change feed, not by noticing that data changed.
Reading geography
get_geo_versions
The version history
get_geo_levels
The levels at a version
get_geo_units
Units at a version, by level and parent, optionally with retired ones
get_geo_unit
One unit at a version, with its ancestors
get_geo_changes
Change events between two versions
get_geo_crosswalk
Successors of a unit between two versions
get_geo_boundary
A level's boundary file at a version
See API reference for requests and responses.
Last updated
Was this helpful?