Reads a YAML file that says which states belong to which regions, which counties belong to which metros, and so on, then:
- downloads only the geoBoundaries layers the YAML needs,
- tags every unit with its groups,
- writes simplified GeoJSON files plus a
layers.jsonthat tells a Leaflet map what to draw.
It skips the work when neither your YAML nor the upstream data has changed.
Python 3.10+, managed with uv.
uv sync # runtime + dev dependencies (pytest, pyright, ...)
uv sync --no-dev # runtime onlyuv run region-import-tool groupings.example.yaml --out-dir out
uv run region-import-tool groupings.example.yaml --out-dir out -c USA # one countryRun uv run region-import-tool --help for the full option list. Running again with nothing changed prints USA: up to date; skipping. It still makes one small metadata request per layer to check for a new upstream version, but downloads nothing; --force rebuilds anyway.
One block per ISO3 country code (validated against ISO 3166-1, plus XKX for Kosovo, which geoBoundaries uses). The whole file is validated up front, so a typo is reported with its exact path before anything is downloaded. See groupings.example.yaml for a complete, commented file.
USA:
levels: # required
states: ADM1
counties: ADM2
regions: # group types: regions, meshregions, metros
- us-midwest:
states: [us-in, us-oh, us-mi]
metros:
- cvg:
counties: # ADM2 members are keyed by their parent
us-oh: [Hamilton, Butler]
us-ky: [Boone, Kenton]
output: { ... } # optional
manifest: { ... } # requiredlevels: is required and maps your names for the admin levels to geoBoundaries' ADM1 / ADM2. Those names become the keys inside groups and the name of the full-layer file, so a country with provinces can say provinces: ADM1. Several labels may point at the same level (the first names the file). The literal keys ADM1 and ADM2 always work. Only ADM1 and ADM2 are supported, and an ADM2 label needs an ADM1 label too (it is the parent).
There are three group types: regions, meshregions and metros. Each is optional, but at least one group must exist. Each unit gets one value per type.
- ADM1 members (
states:) are a list, matched onshapeISO(us-oh) or on name (Ohio), case-insensitively. Use ISO 3166-2 codes with the two-letter country prefix (ca-on, notcan-on). - ADM2 members (
counties:) are a mapping of parent to names.Butlerunderus-ohnever matches Butler County, KY. A trailing "County", "Parish", "Borough" or "Census Area" is ignored when matching. - A group can list both states and counties.
- Group names must be unique across all types, because they become file names.
- A name that matches nothing prints
WARN: no ADM1 match for .../no ADM2 match for ...and the run continues. Read the warnings: that is how typos show up.
Which layers are downloaded follows from the groups: ADM1 only if something lists states or counties, ADM2 only if some group lists counties. geoBoundaries ADM2 has no parent column, so each county's state is found by a spatial join.
output:
simplified: true
simplify: 0.005
precision: 4
members_only: true
base_layers: [states]
dissolve: [metros]| Option | Default | Effect |
|---|---|---|
simplified |
true |
Download geoBoundaries' pre-simplified GeoJSON instead of the full-detail one. |
simplify |
0.005 |
Further simplification in degrees (~500 m). Raise it (0.01, 0.02) if files are still too big. 0 turns it off. |
precision |
4 |
Coordinate decimals (~10 m). |
members_only |
true |
Base layers contain only units that belong to some group (states also kept if they are the parent of a listed county). |
base_layers |
true |
Full layers to write: true, false, or a list of level labels. Group files do not depend on them. |
dissolve |
[] |
Group types whose files are merged into one shape per group instead of one feature per member unit. Values: regions, meshregions, metros. |
A dissolved type cannot be used as the in: source of another layer, since the member units are gone; this is rejected at load time.
Everything is simplified once up front, so all output files share identical edges. If the polygons do not form a clean coverage the tool says so and simplifies each shape separately, which can leave hairline gaps.
The manifest is required. The tool writes one file per group (us-midwest.geojson, cvg.geojson, ...), a single base file for the state layer, and layers.json.
manifest:
# file: layers.json
base_url: /data/usa
layers:
- {id: macro, label: Regions, type: regions, colorBy: feature}
- {id: mesh, label: Regional Meshes, type: meshregions, in: regions}
- {id: state, label: State, type: states, nameProperty: shapeISO}
- {id: metro, label: Metro, type: metros, nameProperty: iata, optional: true}| Key | Meaning |
|---|---|
id, label |
id must be unique. label defaults to the id. |
type |
A group type, or your ADM1 label (states). |
in |
Draw this layer from that group type's files instead of its own. The mesh layer above reuses the region files and groups features by their meshregion property (groupBy is set for you). |
nameProperty |
Property the client reads for a feature's name. If it is not a real column (like iata) it is added to the layer's own files, set to the group name. |
groupBy, colorBy, highlight, coverage, optional, allowOverlap |
Copied through to layers.json. Any other key is copied too. |
base_url (on manifest) |
Prefix added to every url in layers.json. Files on disk keep plain names. Slashes are normalised. |
url and codes are generated. codes are the group names that actually matched units (for the state layer, the lowercase state codes in its file). A layer's url entries are {url, name} objects when it has no nameProperty, and plain strings otherwise. A state layer without in: points at the single base file (states.geojson), so the client loads one file and highlights by property rather than loading one file per state.
What the files contain:
- Group files: the member units, with
shapeName,shapeISO(lowercased, to matchcodes),parent_isofor counties, and the group columns (region,meshregion,metro). Columns that are entirely empty are left out. - Dissolved group files: one feature with the group column and the
nameProperty. - Groups are built from their own definition, so they can overlap (Butler County can be in both
cvgandday). - Two groups whose names map to the same file name stop the run, naming both.
out/
USA/
states.geojson # base layer, members only
us-midwest.geojson # one per group the manifest uses
cvg.geojson
layers.json
uv run pytest tests # no network needed
uv run pyrightTests build a small fake geography (five state boxes, seven counties, two of them named "Butler County") and never touch the network.
Boundary data comes from geoBoundaries (gbOpen, CC BY 4.0). Please cite:
Runfola, D. et al. (2020) geoBoundaries: A global database of political administrative boundaries. PLoS ONE 15(4): e0231866. https://doi.org/10.1371/journal.pone.0231866
Use the attribution text you already have in your README if it differs.