From 22805a3be68015229bce65ede0c2e060ea0bb774 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Thu, 15 Oct 2026 00:17:19 +0000 Subject: [PATCH] =?UTF-8?q?v0.28.0=20(#46):=20per-region=20stats=20?= =?UTF-8?q?=E2=80=94=20area=C3=97population-weighted-median=20Nation.stats?= =?UTF-8?q?=20(PR=201=20of=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 55 +- CHANGELOG.md | 24 + package.json | 4 +- public/ne-admin0-pop.json | 251 ++ public/region-geo.json | 7202 ++++++++++++++++++++++++++++++++ scripts/build-region-geo.mjs | 165 + scripts/extract-admin0-pop.mjs | 78 + scripts/region-geo.test.ts | 139 + server/db.test.ts | 38 + server/db.ts | 10 +- server/engine.test.ts | 226 +- server/engine.ts | 76 +- server/index.ts | 16 +- server/map.test.ts | 2 + server/protocol.test.ts | 220 + server/regions.test.ts | 38 + server/regions.ts | 64 +- shared/protocol.ts | 138 +- shared/types.ts | 26 + 19 files changed, 8743 insertions(+), 29 deletions(-) create mode 100644 public/ne-admin0-pop.json create mode 100644 public/region-geo.json create mode 100644 scripts/build-region-geo.mjs create mode 100644 scripts/extract-admin0-pop.mjs create mode 100644 scripts/region-geo.test.ts create mode 100644 server/protocol.test.ts diff --git a/AGENTS.md b/AGENTS.md index 90f9797..8ec53d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ history/ ├── eslint.config.js # flat config (ESLint 9) ├── tsconfig.json # strict; aliases @/ → src/, @shared/ → shared/ ├── vite.config.ts # dev server + proxy /api,/sse → :4321; @ / @shared aliases -├── vitest.config.ts # unit tests (server/**/*.test.ts + scripts/**/*.test.ts; 189 tests) +├── vitest.config.ts # unit tests (server/**/*.test.ts + scripts/**/*.test.ts; 225 tests) ├── playwright.config.ts # deterministic e2e (webServer runs npm run dev, LLM_STUB=1) ├── index.html ├── presets/ # 5 scenario JSON files + index.json (manifest) @@ -65,28 +65,35 @@ history/ │ ├── regions.geo.json # 1,800 sub-regions, id=-, parent=ISO_A3 (the choropleth) │ ├── region-keys.json # feature id -> region key, 1,800 entries (the ownership/LLM address space, #16 P1) │ ├── region-adjacency.json # region key -> bordering region keys, 1,800 entries (the getNeighbours data, #16 P2) -│ └── region-centroids.json # region key -> {lat,lon,spread} anchor for map markers, 1,800 entries (#44) +│ ├── region-centroids.json # region key -> {lat,lon,spread} anchor for map markers, 1,800 entries (#44) +│ ├── region-geo.json # region key -> {area (km²), population}, 1,800 entries (the area×population weighting for per-region stats, #46) +│ └── ne-admin0-pop.json # ISO_A3 -> admin-0 POP_EST, 249 codes (the committed population SOURCE for region-geo.json — a small derived table, the 13 MB Natural Earth source is NOT committed, #46) ├── scripts/ # the geo/manifest data pipeline — .mjs (turf allowed HERE only; │ │ # never imported by .ts): generate → coarsen → cleanup → smooth → │ │ # fill-gaps → split-arkhangel → split-arkhangel-band → build-region-keys -│ │ # → build-region-adjacency → build-region-centroids +│ │ # → build-region-adjacency → build-region-centroids → build-region-geo +│ │ # (build-region-geo reads the COMMITTED ne-admin0-pop.json; that small +│ │ # table is distilled once from the 13 MB Natural Earth admin-0 source by +│ │ # extract-admin0-pop.mjs — the raw source is never committed, #46) │ └── *.test.ts # turf-free vitest tests pinning the dataset + the manifests ├── shared/ # NO runtime deps — the single source of truth │ ├── types.ts # domain types (Nation, GameState, RoundItem, Preset, …) │ └── protocol.ts # LLM TurnResult schema (incl. `unowned`, v0.22 #16 P3, and │ # `nationOps`, v0.23 #16 P4a; v0.24 #16 P4b) + AddAction/AddDiplomacy/RemoveQueueItem/ │ # Jump + NationOp confirm/reject request types + pure helpers +│ # (incl. per-region stats, v0.28 #46: weightedMedian / deriveNationStats / +│ # seedRegionStats / migrateRegionStats) ├── server/ │ ├── index.ts # Express: /api/* (health|action|diplomacy|queue/remove|jump|advisor| │ │ # nation-op/confirm|nation-op/reject|games|map|presets), /sse/:id; reads │ │ # PORT/LLM_*/DATA_DIR -│ ├── engine.ts # pure: prompts / parse+validate / applyTurnResult (sole-ownership + no-man's-land releases, v0.22 #16 P3) / runTurn (tool path + plain fallback) / getNeighbours tool + executor / playerFrontier / queue ops / advisor / nation-op ops (resolveNationRef / freshNationId / NEW_NATION_STATS / applyNationOp rename+split+merge / confirmNationOp / rejectNationOp, v0.23 #16 P4a; v0.24 #16 P4b) +│ ├── engine.ts # pure: prompts / parse+validate / applyTurnResult (sole-ownership + no-man's-land releases, v0.22 #16 P3; per-region stats maintenance + derived recompute, v0.28 #46) / runTurn (tool path + plain fallback) / getNeighbours tool + executor / playerFrontier / queue ops / advisor / nation-op ops (resolveNationRef / freshNationId / NEW_NATION_STATS / applyNationOp rename+split+merge / confirmNationOp / rejectNationOp, v0.23 #16 P4a; v0.24 #16 P4b) │ ├── llm.ts # LlmClient: chat() + chatWithTools() (bounded function-calling loop), injectable transport, retry+backoff, logging + env config -│ ├── regions.ts # turf-free region-key data access: load manifest/geo/adjacency (public/ then dist/), country→keys expansion, reference table, neighboursOf/hasAdjacency +│ ├── regions.ts # turf-free region-key data access: load manifest/geo/adjacency/centroids/geo-manifest (public/ then dist/), country→keys expansion, reference table, neighboursOf/hasAdjacency, centroidOf/hasCentroids, geoOf/hasGeo (#46) │ ├── stub.ts # deterministic LLM transport (tests/e2e): one frontier conquest per turn + ADVISOR_MARKER │ ├── map.ts # GameState → RegionMap (region key → owner + stable color) │ ├── presets.ts # load preset JSON files from disk -│ ├── db.ts # SQLite open/save/load +│ ├── db.ts # SQLite open/save/load (+ legacy regionStats migration on load, v0.28 #46) │ └── *.test.ts # unit tests (vitest) └── src/ ├── App.tsx # screen router (home → pick → game) + manual hash router (#/game/); owns game/regions/advisors/busy @@ -116,13 +123,15 @@ history/ | `npm run dev` | Express on :4321 (tsx watch) + Vite on :5173 (proxies `/api`,`/sse`) concurrently | | `npm run build` | `vite build` (client → `dist/`) then `tsup server/index.ts` → `server/dist/index.js` | | `npm start` | run the compiled server (`node server/dist/index.js`) on :4321 | -| `npm test` | vitest unit tests (`server/` + `scripts/`; 189 tests) | +| `npm test` | vitest unit tests (`server/` + `scripts/`; 225 tests) | | `npm run typecheck` | `tsc --noEmit` (strict) across src/server/shared/e2e/configs | | `npm run lint` | ESLint flat config | | `npm run e2e` | Playwright, fully deterministic (`LLM_STUB=1`); **runs in CI on every PR** (see below) | | `npm run split:regions` | (re)apply the #31 Arkhangel'sk split (`RU-ARK` → mainland + `RU-ARK-NE` + `RU-ARK-SZ`) — idempotent no-op once applied | | `npm run split:regions:band` | (re)apply the #41 Arctic-coast-band split (`RU-ARK` mainland's 28-part lat ≥ 68 band → `RU-PEO` + `RU-YAM`) — idempotent no-op once applied | | `npm run build:region-keys` / `build:region-adjacency` / `build:region-centroids` | (re)generate the `public/region-keys.json` / `region-adjacency.json` / `region-centroids.json` manifests — deterministic + idempotent (byte-identical re-run) | +| `npm run build:region-geo` | (re)generate `public/region-geo.json` (region key → area km² + population) from `regions.geo.json` + `region-keys.json` + the committed `ne-admin0-pop.json` — deterministic + idempotent (byte-identical re-run; #46) | +| `npm run extract:admin0-pop` | one-time distillation of the 13 MB Natural Earth admin-0 source → the committed `public/ne-admin0-pop.json` (ISO_A3 → `POP_EST`, 249 codes); the raw source is never committed (#46) | | `docker compose up -d --build` | prod image on :4321, routed via Traefik at `history.dev.kkhost.pl` | **Local verification (before every PR):** `npm run typecheck` + `npm run lint` + `npm test` (unit) + `npm run build`. The full **e2e** suite (`npm run e2e`) is verified by **CI on every PR** (the Playwright config boots its own dev webServer with `LLM_STUB=1`) — locally you only need the four checks above; e2e is the CI source of truth. @@ -286,6 +295,38 @@ history/ The **stub is marker-free** (`markers: { add: [], remove: [] }`) — e2e (CI, `LLM_STUB=1`) is unaffected and the marker path is unit-tested only (`server/engine.test.ts`, `scripts/region-centroids.test.ts`, `server/regions.test.ts`, `server/db.test.ts`). +- **Per-region stats — `GameState.regionStats`, derived `Nation.stats` (v0.28, #46; PR 1 of 2 — the LLM lever is PR 2).** + Since v0.28 the four 0–100 stats live at **region-key** granularity: `GameState.regionStats` + (required; `{}` at game creation) maps each owned region key to its own `RegionStats`, and + `Nation.stats` is **derived** — the **area×population-weighted median** of the stats of the + regions the nation owns (`deriveNationStats`, pure in `shared/protocol.ts`). A region's weight + is `(area>0?area:1)×(population>0?population:1)` from the committed `public/region-geo.json` + manifest (1,800 entries; area in km² at 2dp, integer population) — which is built by + `scripts/build-region-geo.mjs` from `regions.geo.json` + `region-keys.json` + the committed + `public/ne-admin0-pop.json` (Natural Earth admin-0 `POP_EST`, 249 codes, one-time distilled by + `scripts/extract-admin0-pop.mjs`; each country's `POP_EST` is spread over its sub-regions + proportional to area by largest-remainder, so per-country totals are preserved exactly). A + released / 0-owner / 2+-owner key has **no** entry. A fresh game is seeded by + `seedRegionStats` (every owned region gets a **copy** of its owner's stats — identical values, + so the derived median equals the original and fresh-game numbers are unchanged). Legacy SQLite + rows are migrated once on load by `migrateRegionStats` (`db.load`, called without `geoOf` — + weight-invariant for the identical-value seed): a row that already has `regionStats` (v0.28+) + is returned **unchanged** (never recomputed on load → no drift), a legacy row is seeded + + recomputed (0-region guard keeps its stats). `applyTurnResult` takes an optional `geoOf` + (passed through `runTurn`; the jump route wires it via `hasGeo() ? (key) => geoOf(key) : + undefined`) and maintains the table inside the **load-bearing order events → relations → + territory → stateChanges → recompute → markers → nationOps**: a release deletes the key's + entry; a (re-)claim re-inits an absent entry as a copy of the gainer's current `stats`; + nation-level `stateChanges` (the only LLM lever for now — the prompt is **unchanged** in this + PR) write to `n.stats` **and** to every region the nation owns; then every nation with ≥1 + entered region gets `stats` recomputed as its regions' weighted median (a 0-region — or no- + entered-region — nation keeps its directly-maintained `stats`, which is why the existing unit + and e2e assertions pass unchanged; without the geo manifest the recompute degrades to an + unweighted median, never throwing). The stub never touches per-region stats (`LLM_STUB=1` + e2e unaffected), and the path is unit-tested (`server/engine.test.ts`, + `server/protocol.test.ts`, `server/db.test.ts`, `server/regions.test.ts`, + `scripts/region-geo.test.ts`). **PR 1 of 2 — deliberately NOT deployed** (the user deploys + when PR 2, the per-region LLM lever, ships). - **Map click → dossier target (owner resolved from the RegionMap, by region key).** Clicking a sub-region does NOT open a popup — it switches the dossier to that sub-region's owning nation (`focusNation(ownerId, {name})` in `App.tsx`, which also records which region was diff --git a/CHANGELOG.md b/CHANGELOG.md index 65feea0..e65fccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ All notable changes to **History** are listed here, newest first. +## v0.28.0 — 2026-10-15 00:12 +- **#46 (PR 1 of 2): per-region stats — a nation's four 0–100 stats become a *derived* aggregate.** Instead of storing `Nation.stats` as four independent per-nation numbers, they are now **derived** as the **area×population-weighted median** of the stats of the regions the nation owns. Each sub-region carries its own `RegionStats` in a new `GameState.regionStats: Record` table; a nation's displayed stats are the weighted median over its owned regions. This is the data foundation for the per-region LLM levers that land in **PR 2** — in this PR the LLM still only emits nation-level `stateChanges`, which are re-interpreted as applying to *all* of a nation's owned regions. A new `public/region-geo.json` manifest (area km² + population per region key) supplies the weighting. **Not deployed in this PR** — the production site updates when PR 2 ships (the change is additive and backward-compatible, so deferring the deploy is safe). + - **Data build (`scripts/`, turf-`.mjs` only; never imported by `.ts`).** + - `scripts/extract-admin0-pop.mjs` (`npm run extract:admin0-pop`) — **one-time** distillation of the 13 MB Natural Earth 1:10m admin-0 GeoJSON (a *source* file we do **not** commit) into the small committed `public/ne-admin0-pop.json` = `{ ADM0_A3 → max POP_EST }` (249 country codes, total 7,677,059,722). The 1:10m **admin-1** file the region generator uses carries no population, so the source is the **admin-0** `POP_EST` (a deliberate deviation, recorded here). + - `scripts/build-region-geo.mjs` (`npm run build:region-geo`) — builds `public/region-geo.json` = `{ regionKey → { area, population } }`: `area` = `@turf/area(finalFeature)/1e6` km² (2 dp); `population` = the parent country's `POP_EST` spread across its sub-regions **proportional to area via largest-remainder**, so every country's sub-regions sum **exactly** to its total (no rounding drift). Reads the committed `ne-admin0-pop.json` → fully reproducible in CI. **Deterministic + idempotent** (sorted keys; byte-identical re-run, pinned by the test). 1,800 entries; **total population 7,654,092,021** (the sum over the 177 repo countries). + - **Types (`shared/types.ts`, no runtime deps).** New `RegionStats = { military, economy, stability, population }` (0–100). `GameState.regionStats: Record` is **required** (`{}` at game creation; a released / 0-owner / 2+owner region has **no** entry; a legacy SQLite row without the field is migrated on load, never wiped). + - **Pure helpers (`shared/protocol.ts`, no deps).** + - `weightedMedian(values, weights)` — sorts by value and returns the value at the first point the cumulative weight reaches half the total; non-finite/non-positive weights are excluded; empty / zero-total → 0. **Translation-invariant** (the median of identical values is that value — the invariant seeding and migration rely on). + - `deriveNationStats(regionKeys, regionStats, geoOf?)` — each stat is the weighted median over the nation's owned region keys; a region's weight is `(area>0?area:1)×(population>0?population:1)` (both fall back to 1 when the key has no geo entry, so missing geo degrades to a uniform median); a key with no `regionStats` entry is skipped; returns zero stats when no owned region has an entry (the caller's 0-region guard keeps `n.stats` in that case). + - `seedRegionStats(nations)` — seeds the table from nations: every owned key gets a **copy** of its owner's stats (first owner wins on a corrupt double-ownership), so a fresh nation's regions start identical (derived = seeded). + - `migrateRegionStats(state, geoOf?)` — **one-time** legacy migration: a row that already has `regionStats` is returned **unchanged** (no recompute on load → no drift); a legacy row (field absent) is seeded per region and each nation's stats recomputed (0-region guard). Never throws. + - **Server (`server/regions.ts`, turf-free).** Loads `region-geo.json` (probes `public/` then `dist/`) **optionally** — a missing/broken file degrades weighting to uniform (every region weight 1) while everything else keeps working. New `hasGeo()` and `geoOf(key)` accessors mirror the existing `hasCentroids`/`centroidOf`. + - **Server (`server/engine.ts`).** `applyTurnResult` gains an optional `geoOf` param and is reordered to: events → relations → **territory** → **stateChanges** → **recompute** → markers → nationOps. + - **Territory:** a **transferred** region **keeps its own** per-region stats (the conquest is what makes a nation's regions diverge); a **released** region's entry is **deleted** (no man's land carries none); a **re-claimed** region with no entry is **re-inited** to the gainer's current stats. + - **StateChanges (re-interpreted):** a nation-level `set`/`add` applies to `n.stats` **and** to every region the nation owns (clamped 0–100). + - **Recompute:** every nation's `stats` is re-derived via `deriveNationStats` (weighted by area×population when `geoOf` is present). Two guards: a **0-region** nation keeps its directly-maintained stats, and a nation **none of whose regions has an entry** keeps its stats too (avoids clobbering with zeros). + - `runTurn` threads the optional `geoOf` through to `applyTurnResult`. + - **Server (`server/index.ts`).** Game creation seeds `regionStats` via `seedRegionStats` **after** `expandCountriesToKeys` (so regions hold real keys) — a new game's derived stats equal its nation-level stats on turn 1. The `jump` route passes `geoOf: hasGeo() ? geoOf : undefined`. + - **Server (`server/db.ts`).** `load()` migrates a legacy row (regionStats absent) via `migrateRegionStats(state)` **without** `geoOf` (uniform weights — weight-invariant for the equal-value seeding, and avoids a `db`→`regions` import). v0.28+ rows load unchanged. + - **Prompt / client / stub unchanged.** `nationForPrompt`/`buildUserPrompt` still surface the (now derived) four stats; there is no per-region LLM lever yet (PR 2). The deterministic stub is unaffected (nation-level `stateChanges` flow through the re-interpretation; the median of identical values is that value, so e2e stat assertions hold). + - **Tests (189 → 225).** New `server/protocol.test.ts` (weightedMedian incl. translation-invariance / identical / zero / empty; deriveNationStats weighted vs uniform / skip-missing / zero / single / fallback-weight; seedRegionStats; migrateRegionStats v0.28+ no-op vs legacy / 0-region); `server/engine.test.ts` (transfer keeps region stats + aggregate blends, release deletes entry, re-claim re-inits, 0-region keeps stats, geoOf-weighted vs uniform recompute, nation-level change re-applies to regions, original not mutated); `server/db.test.ts` (legacy row → seeded + preserved stats; v0.28+ row loads unchanged); `server/regions.test.ts` (`hasGeo`/`geoOf` in dev + prod-`dist/` layouts, graceful degradation when absent); new `scripts/region-geo.test.ts` (1,800-entry key-set match, sorted, finite / ≤2dp area / integer population, total 7,654,092,021, per-country totals preserved vs admin-0, spot-checked values, byte-identical re-run). + - **Verification.** Typecheck, lint, **225 unit tests**, and the production build all pass (the build copies `region-geo.json` + `ne-admin0-pop.json` into `dist/` like the other manifests, so the prod container layout is covered). e2e is verified by CI on the PR (no local run). + - **Deploy:** **none** in this PR — the production site (`https://history.dev.kkhost.pl`) updates when **PR 2** ships. The change is additive + backward-compatible, so existing games migrate on load and no data wipe is needed. + ## v0.27.0 — 2026-10-14 22:34 - **#44: the LLM can place named point markers on the map.** The model may now place up to a handful of named *point markers* per turn (a capital, city, factory complex, army deployment, port, fleet concentration, rocket launch site, or a political **gathering**), each anchored to a region key, persisted per game, and rendered on the choropleth as a MapLibre symbol layer. - **Types (`shared/types.ts` + `shared/protocol.ts`).** Eight `MarkerType`s — `city, factory, army, port, capital, fleet, rocket, gathering` — with a shared `MARKER_TYPE_INFO` map (display label + color + glyph). A `MapMarker = { id, type, name, regionKey, lat, lon, ownerNationId, placedTurn }`; `GameState.markers: MapMarker[]` is **required** (`[]` at game creation, and a legacy SQLite row without the field loads as `[]` — additive, never wipes existing markers). `TurnResult.markers = { add: MarkerSpec[]; remove: string[] }` (both required, default `[]`); `MarkerSpec = { type, name, regionKey }`. diff --git a/package.json b/package.json index 53bbdf9..3ec0fbd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "history", - "version": "0.27.0", + "version": "0.28.0", "private": true, "type": "module", "description": "AI-driven alternate-history simulator — the LLM is the world engine (Pax Historia style).", @@ -19,6 +19,8 @@ "build:region-keys": "node scripts/build-region-keys.mjs", "build:region-adjacency": "node scripts/build-region-adjacency.mjs", "build:region-centroids": "node scripts/build-region-centroids.mjs", + "build:region-geo": "node scripts/build-region-geo.mjs", + "extract:admin0-pop": "node scripts/extract-admin0-pop.mjs", "typecheck": "tsc --noEmit", "lint": "eslint .", "e2e": "playwright test" diff --git a/public/ne-admin0-pop.json b/public/ne-admin0-pop.json new file mode 100644 index 0000000..2adca56 --- /dev/null +++ b/public/ne-admin0-pop.json @@ -0,0 +1,251 @@ +{ + "ABW": 106314, + "AFG": 38041754, + "AGO": 31825295, + "AIA": 14731, + "ALB": 2854191, + "ALD": 29884, + "AND": 77142, + "ARE": 9770529, + "ARG": 44938712, + "ARM": 2957731, + "ASM": 55312, + "ATA": 4490, + "ATF": 140, + "ATG": 97118, + "AUS": 25364307, + "AUT": 8877067, + "AZE": 10023318, + "BDI": 11530580, + "BEL": 11484055, + "BEN": 11801151, + "BFA": 20321378, + "BGD": 163046161, + "BGR": 6975761, + "BHR": 1641172, + "BHS": 389482, + "BIH": 3301000, + "BLM": 9961, + "BLR": 9466856, + "BLZ": 390353, + "BMU": 63918, + "BOL": 11513100, + "BRA": 211049527, + "BRB": 287025, + "BRN": 433285, + "BTN": 763092, + "BWA": 2303697, + "CAF": 4745185, + "CAN": 37589262, + "CHE": 8574832, + "CHL": 18952038, + "CHN": 1397715000, + "CIV": 25716544, + "CMR": 25876380, + "CNM": 10000, + "COD": 86790567, + "COG": 5380508, + "COK": 17459, + "COL": 50339443, + "COM": 850886, + "CPV": 549935, + "CRI": 5047561, + "CSI": 4, + "CUB": 11333483, + "CUW": 157538, + "CYM": 64948, + "CYN": 326000, + "CYP": 1198575, + "CZE": 10669709, + "DEU": 83132799, + "DJI": 973560, + "DMA": 71808, + "DNK": 5818553, + "DOM": 10738958, + "DZA": 43053054, + "ECU": 17373662, + "EGY": 100388073, + "ERI": 6081196, + "ESB": 7850, + "ESP": 47076781, + "EST": 1326590, + "ETH": 112078730, + "FIN": 5520314, + "FJI": 889953, + "FLK": 3398, + "FRA": 67059887, + "FRO": 48678, + "FSM": 113815, + "GAB": 2172579, + "GBR": 66834405, + "GEO": 3720382, + "GGY": 62792, + "GHA": 30417856, + "GIB": 33701, + "GIN": 12771246, + "GMB": 2347706, + "GNB": 1920922, + "GNQ": 1355986, + "GRC": 10716322, + "GRD": 112003, + "GRL": 56225, + "GTM": 16604026, + "GUM": 167294, + "GUY": 782766, + "HKG": 7507400, + "HND": 9746117, + "HRV": 4067500, + "HTI": 11263077, + "HUN": 9769949, + "IDN": 270625568, + "IMN": 84584, + "IND": 1366417754, + "IOA": 2387, + "IOT": 3000, + "IRL": 4941444, + "IRN": 82913906, + "IRQ": 39309783, + "ISL": 361313, + "ISR": 9053300, + "ITA": 60297396, + "JAM": 2948279, + "JEY": 107800, + "JOR": 10101694, + "JPN": 126264931, + "KAB": 36175, + "KAS": 6000, + "KAZ": 18513930, + "KEN": 52573973, + "KGZ": 6456900, + "KHM": 16486542, + "KIR": 117606, + "KNA": 52834, + "KOR": 51709098, + "KOS": 1794248, + "KWT": 4207083, + "LAO": 7169455, + "LBN": 6855713, + "LBR": 4937374, + "LBY": 6777452, + "LCA": 182790, + "LIE": 38019, + "LKA": 21803000, + "LSO": 2125268, + "LTU": 2786844, + "LUX": 619896, + "LVA": 1912789, + "MAC": 640445, + "MAF": 38002, + "MAR": 36471769, + "MCO": 38964, + "MDA": 2657637, + "MDG": 26969307, + "MDV": 530953, + "MEX": 127575529, + "MHL": 58791, + "MKD": 2083459, + "MLI": 19658031, + "MLT": 502653, + "MMR": 54045420, + "MNE": 622137, + "MNG": 3225167, + "MNP": 57216, + "MOZ": 30366036, + "MRT": 4525696, + "MSR": 4649, + "MUS": 1265711, + "MWI": 18628747, + "MYS": 31949777, + "NAM": 2494530, + "NCL": 287800, + "NER": 23310715, + "NFK": 2169, + "NGA": 200963599, + "NIC": 6545502, + "NIU": 1620, + "NLD": 17332850, + "NOR": 5347896, + "NPL": 28608710, + "NRU": 12581, + "NZL": 4917000, + "OMN": 4974986, + "PAK": 216565318, + "PAN": 4246439, + "PCN": 54, + "PER": 32510453, + "PGA": 100, + "PHL": 108116615, + "PLW": 18008, + "PNG": 8776109, + "POL": 37970874, + "PRI": 3193694, + "PRK": 25666161, + "PRT": 10269417, + "PRY": 7044636, + "PSX": 4685306, + "PYF": 279287, + "QAT": 2832067, + "ROU": 19356544, + "RUS": 144373535, + "RWA": 12626950, + "SAH": 603253, + "SAU": 34268528, + "SDN": 42813238, + "SDS": 11062113, + "SEN": 16296364, + "SGP": 5703569, + "SGS": 30, + "SHN": 4534, + "SLB": 669823, + "SLE": 7813215, + "SLV": 6453553, + "SMR": 33860, + "SOL": 5096159, + "SOM": 10192317, + "SPM": 5997, + "SRB": 6944975, + "STP": 215056, + "SUR": 581363, + "SVK": 5454073, + "SVN": 2087946, + "SWE": 10285453, + "SWZ": 1148130, + "SXM": 40733, + "SYC": 97625, + "SYR": 17070135, + "TCA": 38191, + "TCD": 15946876, + "TGO": 8082366, + "THA": 69625582, + "TJK": 9321018, + "TKM": 5942089, + "TLS": 1293119, + "TON": 104494, + "TTO": 1394973, + "TUN": 11694719, + "TUR": 83429615, + "TUV": 11646, + "TWN": 23568378, + "TZA": 58005463, + "UGA": 44269594, + "UKR": 44385155, + "UMI": 300, + "URY": 3461734, + "USA": 328239523, + "USG": 6000, + "UZB": 33580650, + "VAT": 825, + "VCT": 110589, + "VEN": 28515829, + "VGB": 30030, + "VIR": 106631, + "VNM": 96462106, + "VUT": 299882, + "WLF": 11558, + "WSB": 7850, + "WSM": 197097, + "YEM": 29161922, + "ZAF": 58558270, + "ZMB": 17861030, + "ZWE": 14645468 +} diff --git a/public/region-geo.json b/public/region-geo.json new file mode 100644 index 0000000..b112009 --- /dev/null +++ b/public/region-geo.json @@ -0,0 +1,7202 @@ +{ + "aberdeenshire-kzrn": { + "area": 18792.4, + "population": 4711432 + }, + "abra-cmfg": { + "area": 25900.82, + "population": 8525063 + }, + "abu-dhabi-mywn": { + "area": 61234.1, + "population": 8052443 + }, + "abyan-oexn": { + "area": 17717.86, + "population": 1117500 + }, + "aceh-ocsz": { + "area": 61104.2, + "population": 8335786 + }, + "acklins-qehg": { + "area": 560.78, + "population": 10859 + }, + "acre-ekzw": { + "area": 155725.92, + "population": 3831699 + }, + "ad-dakhliyah-qtih": { + "area": 31945.36, + "population": 501990 + }, + "adamaoua-kqsc": { + "area": 62130.88, + "population": 3396478 + }, + "adamawa-ehbv": { + "area": 33825.43, + "population": 7363028 + }, + "adana-wdhs": { + "area": 25397.79, + "population": 2625208 + }, + "adrar-ctwy": { + "area": 477681.66, + "population": 8851331 + }, + "adrar-dffe": { + "area": 222418.66, + "population": 964292 + }, + "afar-cnlj": { + "area": 95416.07, + "population": 9425265 + }, + "afyonkarahisar-gghu": { + "area": 23460.48, + "population": 2424960 + }, + "agadez-ijfb": { + "area": 624229.56, + "population": 12261228 + }, + "agcabadi-yazh": { + "area": 32064.47, + "population": 3668709 + }, + "agneby-glsq": { + "area": 23628.3, + "population": 1853365 + }, + "agri-cvhn": { + "area": 14047.06, + "population": 1451955 + }, + "agusan-del-sur-efza": { + "area": 22594.06, + "population": 7436667 + }, + "ahal-weco": { + "area": 100136.29, + "population": 1248935 + }, + "ain-lqtt": { + "area": 22557.77, + "population": 2304126 + }, + "aisen-del-general-carlos-ibanez-del-campo-mrpn": { + "area": 121123.95, + "population": 2713841 + }, + "aisne-gstu": { + "area": 12888.85, + "population": 1316511 + }, + "ajdabiya-ikqv": { + "area": 196738.01, + "population": 816929 + }, + "akershus-ityh": { + "area": 12899.6, + "population": 159615 + }, + "akita-mmft": { + "area": 27134.14, + "population": 8047348 + }, + "aksaray-cbcb": { + "area": 14437.27, + "population": 1492289 + }, + "al-anbar-yjqa": { + "area": 138351.14, + "population": 12381662 + }, + "al-bahah-ajsr": { + "area": 10841.34, + "population": 191971 + }, + "al-bahr-al-ahmar-oaom": { + "area": 199459.9, + "population": 19741695 + }, + "al-basrah-lrsw": { + "area": 17050.11, + "population": 1525890 + }, + "al-batnah-north-ilnl": { + "area": 14820.69, + "population": 232893 + }, + "al-buhayrah-ilnf": { + "area": 16800.61, + "population": 1662853 + }, + "al-butnan-gytn": { + "area": 79478.65, + "population": 330025 + }, + "al-dhahira-eqif": { + "area": 28891.1, + "population": 453995 + }, + "al-fayyum-wdgf": { + "area": 10797.46, + "population": 1068687 + }, + "al-hudaydah-aubl": { + "area": 18039.41, + "population": 1137781 + }, + "al-hudud-ash-shamaliyah-kdqu": { + "area": 124976.94, + "population": 2213002 + }, + "al-jahrah-cvgx": { + "area": 18389.48, + "population": 4207083 + }, + "al-jawf-misq": { + "area": 118576.61, + "population": 2099669 + }, + "al-jawf-wngz": { + "area": 39349.17, + "population": 2481829 + }, + "al-jizah-abuz": { + "area": 37540.87, + "population": 3715636 + }, + "al-jufrah-mrvm": { + "area": 115958.37, + "population": 481502 + }, + "al-kufrah-ehbh": { + "area": 424777.9, + "population": 1763835 + }, + "al-madinah-uwqp": { + "area": 145884.54, + "population": 2583218 + }, + "al-mahrah-gatz": { + "area": 64496.26, + "population": 4067904 + }, + "al-marj-ydhn": { + "area": 35356.93, + "population": 146815 + }, + "al-minya-mjfl": { + "area": 19516.16, + "population": 1931626 + }, + "al-muthannia-pffh": { + "area": 52943.09, + "population": 4738114 + }, + "al-quassim-imqj": { + "area": 54364.88, + "population": 962654 + }, + "al-qubbah-wlez": { + "area": 21450.97, + "population": 89072 + }, + "al-wadi-at-jadid-spvg": { + "area": 454412.18, + "population": 44975790 + }, + "al-wusta-mdok": { + "area": 65215.83, + "population": 1024802 + }, + "alabama-uqeu": { + "area": 136804.3, + "population": 4694705 + }, + "alagoas-ivzd": { + "area": 28711.26, + "population": 706452 + }, + "alajuela-vwmk": { + "area": 12360.47, + "population": 1148452 + }, + "alaotra-mangoro-lper": { + "area": 27723.37, + "population": 1237568 + }, + "alaska-qfen": { + "area": 1554452.26, + "population": 53344048 + }, + "alba-kbti": { + "area": 18721.32, + "population": 1526848 + }, + "albacete-attf": { + "area": 15011.39, + "population": 1357965 + }, + "alberta-zopq": { + "area": 662346.98, + "population": 2446166 + }, + "aleppo-cqlh": { + "area": 18797.48, + "population": 1708131 + }, + "alessandria-shfc": { + "area": 12827.67, + "population": 2410519 + }, + "alibori-smbm": { + "area": 26519.26, + "population": 2649036 + }, + "allier-butv": { + "area": 14150.41, + "population": 1445370 + }, + "almaty-waxb": { + "area": 221432.48, + "population": 1510742 + }, + "alpes-de-haute-provence-svtw": { + "area": 23412.97, + "population": 2391480 + }, + "alta-verapaz-myed": { + "area": 10457.18, + "population": 1573186 + }, + "altay-mfuq": { + "area": 171008.18, + "population": 1444840 + }, + "alto-paraguay-ezmh": { + "area": 79425.09, + "population": 1386658 + }, + "alto-parana-mimn": { + "area": 14021.19, + "population": 244792 + }, + "amambay-kemk": { + "area": 12894.97, + "population": 225129 + }, + "amapa-gozp": { + "area": 142004.69, + "population": 3494082 + }, + "amazonas-isdt": { + "area": 109647.43, + "population": 4788989 + }, + "amazonas-nthu": { + "area": 41114.72, + "population": 1015712 + }, + "amazonas-qfqs": { + "area": 181147.15, + "population": 5550488 + }, + "amazonas-qtzc": { + "area": 1570573.51, + "population": 38644592 + }, + "ambeno-kpna": { + "area": 628.17, + "population": 50210 + }, + "amhara-kylq": { + "area": 156403.72, + "population": 15449666 + }, + "amman-avrc": { + "area": 22956.14, + "population": 2607319 + }, + "amoron-i-mania-ulvj": { + "area": 16930.83, + "population": 755790 + }, + "ampara-oqcc": { + "area": 13198.7, + "population": 4119306 + }, + "amur-clbp": { + "area": 359608.91, + "population": 3038319 + }, + "amuria-izaz": { + "area": 15521.91, + "population": 2807725 + }, + "amuru-fprn": { + "area": 20138.08, + "population": 3642734 + }, + "an-najaf-buiy": { + "area": 29407.29, + "population": 2631790 + }, + "an-nuqat-al-khams-ehkx": { + "area": 22074.07, + "population": 91660 + }, + "analamanga-szdu": { + "area": 16603.73, + "population": 741189 + }, + "analanjirofo-hgdp": { + "area": 23336.16, + "population": 1041724 + }, + "anatoliki-makedonia-kai-thraki-abyd": { + "area": 14519.05, + "population": 1045719 + }, + "ancash-mtdg": { + "area": 36475.47, + "population": 901103 + }, + "andaman-and-nicobar-stzm": { + "area": 6330.42, + "population": 2719234 + }, + "andhra-pradesh-amam": { + "area": 163523.32, + "population": 70241542 + }, + "androy-wzfq": { + "area": 16305.51, + "population": 727877 + }, + "anhui-sanz": { + "area": 140980.24, + "population": 20914082 + }, + "ankara-oyhp": { + "area": 25991.64, + "population": 2686590 + }, + "annobon-eedl": { + "area": 0, + "population": 0 + }, + "anosy-jadv": { + "area": 30486.28, + "population": 1360905 + }, + "antalya-iahq": { + "area": 23648.79, + "population": 2444425 + }, + "antarctica-ccbf": { + "area": 159604.81, + "population": 57 + }, + "antarctica-mvxe": { + "area": 12421973.85, + "population": 4433 + }, + "antioquia-kqan": { + "area": 63274.62, + "population": 2763599 + }, + "antipodes-islands-ynit": { + "area": 46.04, + "population": 785 + }, + "antofagasta-czps": { + "area": 123573.16, + "population": 2768716 + }, + "anuradhapura-jzhh": { + "area": 29747.2, + "population": 9284078 + }, + "anzoategui-mrqa": { + "area": 46864.71, + "population": 1435971 + }, + "aoste-ymff": { + "area": 11300.17, + "population": 2123478 + }, + "apure-gtqb": { + "area": 76439.9, + "population": 2342177 + }, + "apurimac-aghq": { + "area": 21009.4, + "population": 519024 + }, + "aqmola-etpx": { + "area": 146506, + "population": 999550 + }, + "aqtobe-sgpw": { + "area": 301707.03, + "population": 2058422 + }, + "ar-raqqah-lhjy": { + "area": 18860.32, + "population": 1713840 + }, + "ar-rayyan-ynlo": { + "area": 11867.28, + "population": 2832067 + }, + "ar-riyad-gsqn": { + "area": 376282.64, + "population": 6662942 + }, + "arauca-yxsw": { + "area": 24042.76, + "population": 1050098 + }, + "arbil-sbqc": { + "area": 21047.92, + "population": 1883672 + }, + "archipel-des-crozet-oypb": { + "area": 68.86, + "population": 1 + }, + "archipel-des-kerguelen-fnmd": { + "area": 11779.42, + "population": 139 + }, + "ardebil-aain": { + "area": 20578.04, + "population": 1041531 + }, + "ardeche-zatz": { + "area": 16011.45, + "population": 1635464 + }, + "arequipa-avbe": { + "area": 63630.79, + "population": 1571957 + }, + "arges-hxtf": { + "area": 14773.8, + "population": 1204901 + }, + "arhangay-efvw": { + "area": 54910.19, + "population": 113382 + }, + "arica-y-parinacota-mbpk": { + "area": 16447.39, + "population": 368512 + }, + "arizona-igee": { + "area": 295980.56, + "population": 10157148 + }, + "arkansas-itew": { + "area": 137602.5, + "population": 4722097 + }, + "arkhangel-sk-lhbk": { + "area": 305257.7, + "population": 2579108 + }, + "artigas-stkw": { + "area": 12080.25, + "population": 233856 + }, + "artvin-aohz": { + "area": 12596.05, + "population": 1301973 + }, + "arunachal-pradesh-mpao": { + "area": 80385.12, + "population": 34529479 + }, + "arusha-ssrk": { + "area": 38774.04, + "population": 2370139 + }, + "as-sulaymaniyah-fimw": { + "area": 17159.16, + "population": 1535650 + }, + "as-suwayda-jzmr": { + "area": 11597.84, + "population": 1053898 + }, + "ash-sharqiyah-north-qdza": { + "area": 25205.13, + "population": 396074 + }, + "ash-sharqiyah-qlqi": { + "area": 556901.07, + "population": 9861204 + }, + "ash-sharqiyah-south-gkzv": { + "area": 18616.47, + "population": 292539 + }, + "ash-sharqiyah-wcnm": { + "area": 35656.02, + "population": 3529082 + }, + "ash-shati-qbtm": { + "area": 96482.04, + "population": 400629 + }, + "ashanti-abux": { + "area": 25183.9, + "population": 3122327 + }, + "asir-yfso": { + "area": 78094.64, + "population": 1382844 + }, + "assaba-fzfa": { + "area": 36070.09, + "population": 156381 + }, + "assam-dqap": { + "area": 79799.04, + "population": 34277728 + }, + "astrakhan-anec": { + "area": 47045.74, + "population": 397487 + }, + "asturias-usbk": { + "area": 11270.66, + "population": 1019570 + }, + "aswan-sxuz": { + "area": 17262.55, + "population": 1708574 + }, + "at-ta-mim-xbob": { + "area": 10904.27, + "population": 975872 + }, + "atacama-khpo": { + "area": 82511.68, + "population": 1848714 + }, + "atakora-oobn": { + "area": 21287.96, + "population": 2126476 + }, + "atlantico-norte-ieag": { + "area": 33017.04, + "population": 1645079 + }, + "atlantico-sur-ylal": { + "area": 26976.3, + "population": 1344098 + }, + "atsimo-andrefana-qbfr": { + "area": 70359.86, + "population": 3140857 + }, + "atsimo-atsinanana-cjvi": { + "area": 16148.49, + "population": 720867 + }, + "atsinanana-uoeq": { + "area": 22606.17, + "population": 1009137 + }, + "attapu-cxyj": { + "area": 17448.23, + "population": 541306 + }, + "atyrau-ixli": { + "area": 114474.35, + "population": 781011 + }, + "auckland-islands-ujmt": { + "area": 493.61, + "population": 8419 + }, + "aude-pmva": { + "area": 10795.89, + "population": 1102729 + }, + "aust-agder-ufyy": { + "area": 26988.82, + "population": 333951 + }, + "austurland-cdfj": { + "area": 21566.19, + "population": 68728 + }, + "aveyron-kovb": { + "area": 20747.5, + "population": 2119220 + }, + "ayacucho-vhko": { + "area": 44606.8, + "population": 1101982 + }, + "ayeyarwady-mhrr": { + "area": 44541.18, + "population": 3515390 + }, + "azad-kashmir-ajpr": { + "area": 13017.97, + "population": 3199690 + }, + "azores-ayly": { + "area": 1978.31, + "population": 208383 + }, + "azuay-kruk": { + "area": 13015.38, + "population": 869361 + }, + "babil-emst": { + "area": 12228.55, + "population": 1094388 + }, + "bacau-zdvy": { + "area": 12137.77, + "population": 989916 + }, + "bacs-kiskun-udpx": { + "area": 12691.1, + "population": 1326843 + }, + "badajoz-ciwt": { + "area": 22414.28, + "population": 2027648 + }, + "badakhshan-qjrt": { + "area": 44421.83, + "population": 2615000 + }, + "baden-wurttemberg-oyje": { + "area": 36272.75, + "population": 8344478 + }, + "badghis-werz": { + "area": 20580.94, + "population": 1211547 + }, + "bafata-krfg": { + "area": 26378.52, + "population": 1359763 + }, + "baghlan-wqdr": { + "area": 18259.04, + "population": 1074863 + }, + "bagmati-ungl": { + "area": 18089.52, + "population": 3492461 + }, + "bago-uzrm": { + "area": 36853.72, + "population": 2908662 + }, + "bahia-gncm": { + "area": 566505.5, + "population": 13939095 + }, + "baja-california-rkil": { + "area": 79217.32, + "population": 5056663 + }, + "baja-california-sur-uoie": { + "area": 77842.71, + "population": 4968918 + }, + "baja-verapaz-ongg": { + "area": 20265.82, + "population": 3048805 + }, + "bakool-grdf": { + "area": 26380.35, + "population": 551514 + }, + "baleares-ayfx": { + "area": 5238.54, + "population": 473890 + }, + "balikesir-exjy": { + "area": 45619.89, + "population": 4715437 + }, + "balkan-edcn": { + "area": 124171.34, + "population": 1548709 + }, + "balkh-ckka": { + "area": 16149.8, + "population": 950698 + }, + "baluchistan-ihjh": { + "area": 351321.86, + "population": 86351449 + }, + "bamingui-bangoran-ilrg": { + "area": 58340.08, + "population": 444406 + }, + "bamyan-mjkz": { + "area": 17733.04, + "population": 1043899 + }, + "bandundu-gzcq": { + "area": 296970.33, + "population": 11026244 + }, + "bangka-belitung-ixwo": { + "area": 18597, + "population": 2536987 + }, + "banja-luka-qguu": { + "area": 13602.93, + "population": 869774 + }, + "banskobystricky-iono": { + "area": 16231.81, + "population": 1825814 + }, + "banten-stwc": { + "area": 12922.67, + "population": 1762901 + }, + "barcelona-iybx": { + "area": 14225.41, + "population": 1286864 + }, + "barh-el-gazel-nhes": { + "area": 56599.67, + "population": 708979 + }, + "bari-gtie": { + "area": 16990.72, + "population": 3192822 + }, + "bari-sedg": { + "area": 73022.42, + "population": 1526625 + }, + "barima-waini-wifs": { + "area": 25694.13, + "population": 94484 + }, + "barinas-uwqd": { + "area": 36998.62, + "population": 1133666 + }, + "bas-congo-yhcf": { + "area": 55543.17, + "population": 2062268 + }, + "bas-sassandra-muqc": { + "area": 26777.97, + "population": 2100419 + }, + "bashkortostan-uovk": { + "area": 142431.01, + "population": 1203393 + }, + "basse-kotto-earz": { + "area": 16859.99, + "population": 128431 + }, + "batanes-orcp": { + "area": 55.87, + "population": 18388 + }, + "batdambang-jett": { + "area": 13655.7, + "population": 1210703 + }, + "batha-dyem": { + "area": 91428.52, + "population": 1145252 + }, + "batken-cbll": { + "area": 17805.46, + "population": 571900 + }, + "batna-enwu": { + "area": 11999.94, + "population": 222356 + }, + "bauchi-spdb": { + "area": 49761.44, + "population": 10831935 + }, + "bay-of-plenty-wlsa": { + "area": 21689.79, + "population": 369943 + }, + "bay-uvdr": { + "area": 44474.87, + "population": 929802 + }, + "bayan-olgiy-ucvu": { + "area": 45893.7, + "population": 94764 + }, + "bayanhongor-udsh": { + "area": 116897.1, + "population": 241376 + }, + "bayern-aiju": { + "area": 69609.99, + "population": 16013648 + }, + "bazega-ufdx": { + "area": 11422.33, + "population": 840849 + }, + "bechar-kfwp": { + "area": 165677.8, + "population": 3069971 + }, + "beijing-qsqi": { + "area": 16585.91, + "population": 2460480 + }, + "beja-yaie": { + "area": 16571.9, + "population": 1221169 + }, + "beja-ykgw": { + "area": 17135.1, + "population": 1804906 + }, + "bekes-itph": { + "area": 11449.61, + "population": 1197046 + }, + "belait-asmw": { + "area": 8468.66, + "population": 433285 + }, + "belgorod-wxjk": { + "area": 27814.97, + "population": 235007 + }, + "belluno-iojp": { + "area": 21391.61, + "population": 4019817 + }, + "bengkulu-qwwn": { + "area": 21321.49, + "population": 2908661 + }, + "bengo-awxb": { + "area": 46594.38, + "population": 1168408 + }, + "benguela-ifxx": { + "area": 40425.06, + "population": 1013705 + }, + "benshangul-gumaz-iznr": { + "area": 49935.78, + "population": 4932690 + }, + "benue-uwdf": { + "area": 31615.13, + "population": 6881895 + }, + "beqaa-ryfc": { + "area": 10480.9, + "population": 6855713 + }, + "bern-pmiz": { + "area": 17343.92, + "population": 3622503 + }, + "berry-islands-wejq": { + "area": 7614.03, + "population": 147442 + }, + "betsiboka-upag": { + "area": 30737.07, + "population": 1372100 + }, + "beyla-vyfv": { + "area": 12715.98, + "population": 646332 + }, + "bheri-czgc": { + "area": 10798.96, + "population": 2084905 + }, + "bhojpur-uxpy": { + "area": 17430.61, + "population": 3365248 + }, + "bie-usov": { + "area": 72712.29, + "population": 1823344 + }, + "bihar-cbcp": { + "area": 102764.51, + "population": 44142560 + }, + "bilecik-wyjz": { + "area": 13890.39, + "population": 1435761 + }, + "bimini-sdjg": { + "area": 3.92, + "population": 76 + }, + "binh-phuoc-qysb": { + "area": 23825.98, + "population": 6741636 + }, + "bio-bio-kfrw": { + "area": 24788.05, + "population": 555388 + }, + "bioko-sur-yvgu": { + "area": 1891.79, + "population": 86684 + }, + "biskra-oqww": { + "area": 20956.58, + "population": 388321 + }, + "bitlis-gbjb": { + "area": 16757.03, + "population": 1732067 + }, + "blue-nile-conz": { + "area": 44782.32, + "population": 1026111 + }, + "boeny-fmdt": { + "area": 31995.97, + "population": 1428297 + }, + "boke-jifl": { + "area": 11384.36, + "population": 578648 + }, + "bolikhamxai-csag": { + "area": 15971.97, + "population": 495507 + }, + "bolivar-oyqe": { + "area": 252358.85, + "population": 7732469 + }, + "bolivar-shkq": { + "area": 26025.02, + "population": 1136676 + }, + "bologna-otjs": { + "area": 12535.39, + "population": 2355596 + }, + "bolu-cdjc": { + "area": 20705.63, + "population": 2140209 + }, + "bonaire-makp": { + "area": 224.94, + "population": 94529 + }, + "bongolava-irjg": { + "area": 18280.69, + "population": 816048 + }, + "boqueron-uume": { + "area": 89074.58, + "population": 1555125 + }, + "borgou-yqft": { + "area": 26024.02, + "population": 2599565 + }, + "borkou-nuea": { + "area": 249445.5, + "population": 3124606 + }, + "borno-emdz": { + "area": 71124.69, + "population": 15482231 + }, + "bouenza-elwk": { + "area": 12976.66, + "population": 199447 + }, + "boulgou-oyva": { + "area": 21198.53, + "population": 1560519 + }, + "bouvet-island-vssk": { + "area": 12.14, + "population": 150 + }, + "boyaca-uekx": { + "area": 22981.17, + "population": 1003731 + }, + "bozen-ukjd": { + "area": 13538.79, + "population": 2544150 + }, + "brakna-rcfc": { + "area": 33173.51, + "population": 143823 + }, + "brandenburg-yrjj": { + "area": 30767.61, + "population": 7078031 + }, + "branicevski-nitf": { + "area": 18357.12, + "population": 1638501 + }, + "brescia-mzjw": { + "area": 18500.48, + "population": 3476529 + }, + "brest-uhpa": { + "area": 33175.19, + "population": 1517260 + }, + "british-columbia-vmsh": { + "area": 962276.78, + "population": 3553861 + }, + "brong-ahafo-qqwr": { + "area": 39931.06, + "population": 4950694 + }, + "bryansk-hxom": { + "area": 35336, + "population": 298552 + }, + "buenos-aires-mwnv": { + "area": 307667.71, + "population": 4932315 + }, + "bukhoro-udcy": { + "area": 47257.47, + "population": 3542875 + }, + "bukidnon-faet": { + "area": 20557.9, + "population": 6766481 + }, + "bulgan-czsv": { + "area": 49037.85, + "population": 101256 + }, + "burgas-nowy": { + "area": 16141.81, + "population": 987862 + }, + "burgos-asxn": { + "area": 20237.98, + "population": 1830775 + }, + "buri-ram-ghiq": { + "area": 18946.35, + "population": 2499200 + }, + "bursa-ogje": { + "area": 18260.04, + "population": 1887424 + }, + "bururi-uaba": { + "area": 14714.15, + "population": 6183121 + }, + "buryat-wvvy": { + "area": 352318.97, + "population": 2976727 + }, + "bushehr-ksii": { + "area": 25505.31, + "population": 1290919 + }, + "buskerud-onyd": { + "area": 15315.73, + "population": 189512 + }, + "buvuma-gikj": { + "area": 16557.26, + "population": 2995008 + }, + "buzau-ipvp": { + "area": 19592.87, + "population": 1597929 + }, + "caaguazu-xlmh": { + "area": 12739.03, + "population": 222407 + }, + "caazapa-snmi": { + "area": 12845.45, + "population": 224265 + }, + "cabo-delgado-mdft": { + "area": 79330.68, + "population": 3020327 + }, + "caceres-kdze": { + "area": 19823.81, + "population": 1793309 + }, + "cadiz-aazc": { + "area": 16691.73, + "population": 1509973 + }, + "cagayan-sezc": { + "area": 16404.58, + "population": 5399447 + }, + "cajamarca-mjjy": { + "area": 34497.56, + "population": 852240 + }, + "caldas-wkha": { + "area": 11493.12, + "population": 501977 + }, + "california-kikh": { + "area": 413329.21, + "population": 14184195 + }, + "camaguey-ewbu": { + "area": 18711.38, + "population": 1765918 + }, + "camarines-sur-ulzq": { + "area": 23552.02, + "population": 7751974 + }, + "cambridgeshire-fwds": { + "area": 11573.76, + "population": 2901650 + }, + "campbell-islands-ywks": { + "area": 120.34, + "population": 2053 + }, + "campeche-qfna": { + "area": 59599.94, + "population": 3804431 + }, + "campobasso-wmlo": { + "area": 25214.35, + "population": 4738171 + }, + "canindeyu-ocmj": { + "area": 15330.5, + "population": 267650 + }, + "cantal-rgvw": { + "area": 19685.87, + "population": 2010782 + }, + "canterbury-kxye": { + "area": 47187.13, + "population": 804829 + }, + "caprivi-yapw": { + "area": 19999.9, + "population": 60002 + }, + "caqueta-qfhx": { + "area": 90783.02, + "population": 3965062 + }, + "casanare-aihz": { + "area": 44127.54, + "population": 1927326 + }, + "castelo-branco-gcgr": { + "area": 12372.61, + "population": 1303255 + }, + "cat-island-kqms": { + "area": 304.6, + "population": 5899 + }, + "catamarca-ekne": { + "area": 101398.53, + "population": 1625551 + }, + "catania-kqla": { + "area": 19766.66, + "population": 3714464 + }, + "cauca-szht": { + "area": 32292.33, + "population": 1410408 + }, + "cavally-qqst": { + "area": 14444.02, + "population": 1132965 + }, + "cayo-ejvk": { + "area": 22512.42, + "population": 390353 + }, + "ceara-wceu": { + "area": 152485.3, + "population": 3751962 + }, + "central-cczg": { + "area": 31159.33, + "population": 557479 + }, + "central-darfur-wrpe": { + "area": 37626.08, + "population": 862138 + }, + "central-equatoria-sogr": { + "area": 47922.65, + "population": 839372 + }, + "central-finland-cpkp": { + "area": 19203.55, + "population": 313816 + }, + "central-river-qqfp": { + "area": 11412.53, + "population": 2347706 + }, + "central-sqzo": { + "area": 13976.09, + "population": 1245535 + }, + "central-svtv": { + "area": 96217.91, + "population": 2272557 + }, + "central-ypdw": { + "area": 152078.08, + "population": 602844 + }, + "centre-aexz": { + "area": 69146.77, + "population": 3780013 + }, + "centre-kurt": { + "area": 13758.58, + "population": 1899135 + }, + "centro-sur-gwiq": { + "area": 27701.29, + "population": 1269302 + }, + "cerro-largo-sjom": { + "area": 23174.3, + "population": 448621 + }, + "cesar-qcua": { + "area": 22324.92, + "population": 975069 + }, + "chachoengsao-uwfm": { + "area": 23201.12, + "population": 3060442 + }, + "chaco-yfnb": { + "area": 100083.98, + "population": 1604477 + }, + "chagang-do-erdw": { + "area": 15731.56, + "population": 3233157 + }, + "chahar-mahall-and-bakhtiari-uiis": { + "area": 15045.11, + "population": 761489 + }, + "chaiyaphum-acin": { + "area": 13588.95, + "population": 1792508 + }, + "champasak-agcn": { + "area": 15226.36, + "population": 472375 + }, + "chaouia-ouardigha-edem": { + "area": 27120.33, + "population": 1630175 + }, + "chardzhou-mzcb": { + "area": 93587.95, + "population": 1167262 + }, + "charente-maritime-djvy": { + "area": 13275.61, + "population": 1356015 + }, + "chari-baguirmi-aeay": { + "area": 46201.68, + "population": 578732 + }, + "chatham-islands-territory-svse": { + "area": 912.7, + "population": 15567 + }, + "chechnya-qixv": { + "area": 16700.99, + "population": 141106 + }, + "chelyabinsk-qnxc": { + "area": 88071.91, + "population": 744115 + }, + "cher-yyvr": { + "area": 14376.13, + "population": 1468426 + }, + "cherkasy-wzvf": { + "area": 21220.84, + "population": 1638947 + }, + "chernihiv-cevi": { + "area": 31599.19, + "population": 2440497 + }, + "chhattisgarh-yiee": { + "area": 135286.99, + "population": 58112612 + }, + "chiang-mai-qstn": { + "area": 22910.57, + "population": 3022117 + }, + "chiang-rai-uqtm": { + "area": 17553.55, + "population": 2315476 + }, + "chiapas-izef": { + "area": 75052.84, + "population": 4790833 + }, + "chihuahua-imen": { + "area": 248299.46, + "population": 15849649 + }, + "chikwawa-ulnn": { + "area": 14127.53, + "population": 2195025 + }, + "chin-kltn": { + "area": 37019.72, + "population": 2921763 + }, + "chiriqui-mxiy": { + "area": 18849.65, + "population": 1019970 + }, + "chita-qxms": { + "area": 430805.4, + "population": 3639855 + }, + "chitipa-qnno": { + "area": 19733.44, + "population": 3066027 + }, + "chittagong-bxry": { + "area": 29310.69, + "population": 34389056 + }, + "choco-qaew": { + "area": 48602.66, + "population": 2122782 + }, + "chongqing-mcsc": { + "area": 82644.05, + "population": 12260047 + }, + "chontales-uefv": { + "area": 26589.87, + "population": 1324844 + }, + "chubut-apna": { + "area": 230050.82, + "population": 3688015 + }, + "chukchi-autonomous-okrug-mmxs": { + "area": 712677.8, + "population": 6021382 + }, + "chuquisaca-mztf": { + "area": 52326.25, + "population": 551875 + }, + "chuvash-mhxl": { + "area": 18222.97, + "population": 153965 + }, + "chuy-xdlm": { + "area": 19896.86, + "population": 639075 + }, + "cimislia-acda": { + "area": 17015.84, + "population": 1361359 + }, + "ciudad-real-adzn": { + "area": 19406.64, + "population": 1755570 + }, + "clarendon-muxv": { + "area": 12607.63, + "population": 2948279 + }, + "cluj-cgyo": { + "area": 18275.82, + "population": 1490514 + }, + "coahuila-givi": { + "area": 151268.99, + "population": 9655923 + }, + "coast-kjrv": { + "area": 83592.19, + "population": 7449654 + }, + "cochabamba-oqtq": { + "area": 56695.35, + "population": 597955 + }, + "cojedes-abqg": { + "area": 14395.51, + "population": 441089 + }, + "collines-qzhd": { + "area": 14509.53, + "population": 1449371 + }, + "colombia-weor": { + "area": 0, + "population": 0 + }, + "colorado-wfkf": { + "area": 269044.09, + "population": 9232771 + }, + "comayagua-gvxb": { + "area": 19571.16, + "population": 1651478 + }, + "concepcion-vrml": { + "area": 18917.93, + "population": 330282 + }, + "connecticut-apkc": { + "area": 14195.22, + "population": 487137 + }, + "constanta-owyy": { + "area": 24471.82, + "population": 1995839 + }, + "copperbelt-gftb": { + "area": 31377.56, + "population": 741102 + }, + "coquimbo-qtuj": { + "area": 38420.39, + "population": 860827 + }, + "cordoba-cgnl": { + "area": 168438.62, + "population": 2700291 + }, + "cordoba-mxza": { + "area": 13895.32, + "population": 1257003 + }, + "cordoba-srwy": { + "area": 28703.14, + "population": 1253646 + }, + "cork-qnps": { + "area": 19060.67, + "population": 1333516 + }, + "corrientes-ksnc": { + "area": 88866.97, + "population": 1424654 + }, + "corum-amjr": { + "area": 20441.08, + "population": 2112864 + }, + "cosenza-oolz": { + "area": 14893.57, + "population": 2798735 + }, + "cotabato-wzzy": { + "area": 28463.76, + "population": 9368638 + }, + "cote-d-or-zhyz": { + "area": 15302.17, + "population": 1563015 + }, + "cotes-d-armor-qiya": { + "area": 23516.56, + "population": 2402061 + }, + "creuse-lkyb": { + "area": 11161.99, + "population": 1140123 + }, + "crimea-qsnu": { + "area": 30366.15, + "population": 256562 + }, + "cross-river-erfd": { + "area": 27239.41, + "population": 5929401 + }, + "cuando-cubango-sifc": { + "area": 198525, + "population": 4978244 + }, + "cuanza-norte-avuq": { + "area": 24113.83, + "population": 604682 + }, + "cuanza-sul-uays": { + "area": 55505.33, + "population": 1391860 + }, + "cuenca-chzq": { + "area": 17362.35, + "population": 1570639 + }, + "cumbria-oohy": { + "area": 17342.51, + "population": 4347929 + }, + "cundinamarca-ieom": { + "area": 24891.11, + "population": 1087151 + }, + "cunene-etup": { + "area": 77966.83, + "population": 1955108 + }, + "cusco-ypen": { + "area": 75111.37, + "population": 1855577 + }, + "cuvette-ouest-crwo": { + "area": 25729.16, + "population": 395449 + }, + "cuvette-wtpy": { + "area": 45138.35, + "population": 693762 + }, + "dagestan-qfgq": { + "area": 50853.18, + "population": 429656 + }, + "dakhlet-nouadhibou-cifl": { + "area": 18569.6, + "population": 80508 + }, + "dalarna-cluu": { + "area": 30533.97, + "population": 696061 + }, + "darien-iziz": { + "area": 17171.46, + "population": 929162 + }, + "davao-oriental-sxtc": { + "area": 16265.23, + "population": 5353582 + }, + "dayr-az-zawr-nthe": { + "area": 27460.82, + "population": 2495370 + }, + "debub-rtxk": { + "area": 34401.25, + "population": 1673399 + }, + "debubawi-keyih-bahri-jrxo": { + "area": 22586.32, + "population": 1098679 + }, + "delta-amacuro-wdqr": { + "area": 38440.25, + "population": 1177839 + }, + "delta-qnoh": { + "area": 34912.11, + "population": 7599573 + }, + "denguele-ycsk": { + "area": 21412.5, + "population": 1679561 + }, + "denizli-spld": { + "area": 18419.33, + "population": 1903889 + }, + "devon-slaz": { + "area": 14952.89, + "population": 3748830 + }, + "dhaka-gvrx": { + "area": 31408.73, + "population": 36850599 + }, + "dhamar-utko": { + "area": 27661.46, + "population": 1744662 + }, + "dhi-qar-pmxm": { + "area": 14097.94, + "population": 1261687 + }, + "dhofar-gtht": { + "area": 130039.16, + "population": 2043437 + }, + "diana-oydk": { + "area": 20525.69, + "population": 916265 + }, + "diekirch-kyaq": { + "area": 2679.3, + "population": 619896 + }, + "diffa-akfc": { + "area": 145557.42, + "population": 2859065 + }, + "dikhil-zrvh": { + "area": 11181.9, + "population": 483244 + }, + "dinguiraye-vqqn": { + "area": 13227.38, + "population": 672326 + }, + "dix-huit-montagnes-gcqh": { + "area": 25735.95, + "population": 2018685 + }, + "diyala-pzxe": { + "area": 20013.68, + "population": 1791114 + }, + "diyarbakir-mrle": { + "area": 23790.69, + "population": 2459092 + }, + "djak-lak-qdho": { + "area": 18336.83, + "population": 5188464 + }, + "djelfa-uxzd": { + "area": 35262.42, + "population": 653405 + }, + "djien-bien-uwxm": { + "area": 17239.15, + "population": 4877870 + }, + "dnipropetrovs-k-uszu": { + "area": 31636.99, + "population": 2443416 + }, + "dodoma-cvrm": { + "area": 42833.76, + "population": 2618298 + }, + "dolj-yaab": { + "area": 12406.21, + "population": 1011808 + }, + "donets-k-gvzw": { + "area": 26745.32, + "population": 2065618 + }, + "donga-geqd": { + "area": 11090.5, + "population": 1107841 + }, + "dornod-qpsp": { + "area": 122038.38, + "population": 251992 + }, + "dornogovi-assr": { + "area": 114428.54, + "population": 236279 + }, + "dosso-smfd": { + "area": 31448.62, + "population": 617720 + }, + "doubs-xnyd": { + "area": 10401.05, + "population": 1062398 + }, + "doukkala-abda-sghb": { + "area": 18360.61, + "population": 1103637 + }, + "dubay-kskv": { + "area": 13065.04, + "population": 1718086 + }, + "dumfries-and-galloway-sres": { + "area": 31201.9, + "population": 7822609 + }, + "dundgovi-mgqq": { + "area": 74783.28, + "population": 154417 + }, + "durango-gnsc": { + "area": 121297.64, + "population": 7742768 + }, + "durazno-qcxo": { + "area": 12030, + "population": 232883 + }, + "dytiki-ellada-mlyj": { + "area": 18683.91, + "population": 1345689 + }, + "dytiki-makedonia-keyf": { + "area": 18471.07, + "population": 1330359 + }, + "dzavhan-yqqg": { + "area": 82006.25, + "population": 169331 + }, + "east-azarbaijan-qxil": { + "area": 46448.77, + "population": 2350945 + }, + "east-flanders-cicp": { + "area": 18737.9, + "population": 6979530 + }, + "east-kazakhstan-ipgz": { + "area": 279954.68, + "population": 1910015 + }, + "east-new-britain-bphy": { + "area": 32079.68, + "population": 573946 + }, + "east-sepik-azma": { + "area": 42863.37, + "population": 766880 + }, + "eastern-avgq": { + "area": 13743.29, + "population": 6784843 + }, + "eastern-cape-oqpg": { + "area": 170177.06, + "population": 8099391 + }, + "eastern-darfur-kbbu": { + "area": 57839.32, + "population": 1325290 + }, + "eastern-dtaw": { + "area": 15811.65, + "population": 1621525 + }, + "eastern-equatoria-ilgp": { + "area": 68224.99, + "population": 1194971 + }, + "eastern-highlands-ohoa": { + "area": 18038.72, + "population": 322735 + }, + "eastern-irer": { + "area": 35928.95, + "population": 4454509 + }, + "eastern-mxtw": { + "area": 50153.16, + "population": 1184560 + }, + "eastern-qapp": { + "area": 158010.55, + "population": 14081744 + }, + "edo-utql": { + "area": 19739.59, + "population": 4296860 + }, + "el-bayadh-gadi": { + "area": 67913.39, + "population": 1258419 + }, + "el-beni-aptp": { + "area": 211595.53, + "population": 2231659 + }, + "el-oued-oldp": { + "area": 46721.91, + "population": 865746 + }, + "el-paraiso-gxia": { + "area": 14240.94, + "population": 1201697 + }, + "elbasan-bjpz": { + "area": 17119.74, + "population": 1639166 + }, + "enga-yfvk": { + "area": 21855.89, + "population": 391029 + }, + "ennedi-sxtm": { + "area": 191049.9, + "population": 2393130 + }, + "entre-rios-wonq": { + "area": 80057.03, + "population": 1283419 + }, + "enugu-ijqf": { + "area": 12600.91, + "population": 2742932 + }, + "equateur-mkpe": { + "area": 407058.3, + "population": 15113712 + }, + "erongo-mmar": { + "area": 68872.91, + "population": 206628 + }, + "erzincan-qvlh": { + "area": 28731.37, + "population": 2969778 + }, + "erzurum-kxli": { + "area": 24048.34, + "population": 2485723 + }, + "esfahan-wpig": { + "area": 110021.7, + "population": 5568608 + }, + "eskisehir-eslf": { + "area": 13724.27, + "population": 1418590 + }, + "esmeraldas-ikug": { + "area": 16712.02, + "population": 1116278 + }, + "espirito-santo-yopi": { + "area": 50095.37, + "population": 1232617 + }, + "est-umbd": { + "area": 109448.56, + "population": 5983172 + }, + "estuaire-qawm": { + "area": 23551.16, + "population": 188636 + }, + "eure-jqyf": { + "area": 15282.32, + "population": 1560988 + }, + "evora-qfgt": { + "area": 12674.48, + "population": 1335052 + }, + "extreme-nord-oubi": { + "area": 35399.22, + "population": 1935152 + }, + "exuma-raxf": { + "area": 360.86, + "population": 6988 + }, + "f-a-t-a-yhan": { + "area": 27414.85, + "population": 6738301 + }, + "falcon-scqh": { + "area": 29722.24, + "population": 910712 + }, + "falesti-qcpf": { + "area": 16202.37, + "population": 1296278 + }, + "falkland-islands-ubzb": { + "area": 17551.24, + "population": 3398 + }, + "farah-dajq": { + "area": 49352.1, + "population": 2905232 + }, + "faranah-lbvx": { + "area": 13916.12, + "population": 707334 + }, + "fars-wxkv": { + "area": 126398.25, + "population": 6397486 + }, + "faryab-ztoh": { + "area": 20901.47, + "population": 1230416 + }, + "fatick-kohi": { + "area": 11894.85, + "population": 954199 + }, + "ferghana-ammo": { + "area": 19193.51, + "population": 1438930 + }, + "fes-boulemane-cjeq": { + "area": 19518.64, + "population": 1173245 + }, + "finnmark-mdcj": { + "area": 56721.16, + "population": 701849 + }, + "firenze-sksq": { + "area": 10766.18, + "population": 2023134 + }, + "florida-acct": { + "area": 10591.61, + "population": 205038 + }, + "florida-gmqg": { + "area": 153540.47, + "population": 5269040 + }, + "formosa-ctnd": { + "area": 76295.4, + "population": 1223115 + }, + "free-state-idrj": { + "area": 130093.55, + "population": 6191660 + }, + "fujian-gmhk": { + "area": 129466.58, + "population": 19206057 + }, + "fukuoka-ylvc": { + "area": 15483.38, + "population": 4592006 + }, + "fukushima-wpfv": { + "area": 14343.72, + "population": 4254009 + }, + "gafsa-ofsu": { + "area": 13811.27, + "population": 1017741 + }, + "galapagos-sguu": { + "area": 6693.43, + "population": 447087 + }, + "galguduud-arwl": { + "area": 50154.31, + "population": 1048538 + }, + "galway-kjar": { + "area": 14842.96, + "population": 1038439 + }, + "gambela-peoples-sggc": { + "area": 26045.23, + "population": 2572766 + }, + "gandaki-ohsw": { + "area": 29241.09, + "population": 5645445 + }, + "gangwon-uuoi": { + "area": 19591.35, + "population": 9633595 + }, + "gansu-exjm": { + "area": 406051.64, + "population": 60236789 + }, + "gao-htwg": { + "area": 173921.02, + "population": 2715850 + }, + "gaoual-jpxg": { + "area": 16790.23, + "population": 853420 + }, + "gard-guab": { + "area": 16635.85, + "population": 1699242 + }, + "gash-barka-coeg": { + "area": 33853.66, + "population": 1646763 + }, + "gauteng-eruj": { + "area": 18468.35, + "population": 878981 + }, + "gavleborg-ynuf": { + "area": 20964.67, + "population": 477916 + }, + "gaza-mnfq": { + "area": 77196.1, + "population": 2939058 + }, + "gaza-strip-sazs": { + "area": 417.05, + "population": 313761 + }, + "gbapolu-iigq": { + "area": 28155.89, + "population": 1352240 + }, + "gedarif-oxvo": { + "area": 59035.89, + "population": 1352707 + }, + "gedo-yxwp": { + "area": 46571.49, + "population": 973635 + }, + "gegharkunik-qezd": { + "area": 16479.7, + "population": 1627796 + }, + "geita-elvs": { + "area": 24282.24, + "population": 1484300 + }, + "gelderland-ygoe": { + "area": 20465.73, + "population": 8600493 + }, + "georgia-uotf": { + "area": 152728.03, + "population": 5241159 + }, + "gers-wyad": { + "area": 10939.67, + "population": 1117415 + }, + "gezira-chvu": { + "area": 25019.05, + "population": 573269 + }, + "ghadamis-aagv": { + "area": 68609.05, + "population": 284890 + }, + "ghanzi-oamb": { + "area": 115395.6, + "population": 457433 + }, + "ghardaia-czme": { + "area": 76001.54, + "population": 1408291 + }, + "ghat-ebgf": { + "area": 51723.56, + "population": 214776 + }, + "ghazni-nnab": { + "area": 21849.3, + "population": 1286213 + }, + "ghor-bxar": { + "area": 36255.31, + "population": 2134258 + }, + "gia-lai-kyhl": { + "area": 22762.4, + "population": 6440691 + }, + "gifu-agjj": { + "area": 5.48, + "population": 1624 + }, + "gilan-ugkk": { + "area": 15277.22, + "population": 773237 + }, + "giresun-wdlv": { + "area": 13211.55, + "population": 1365593 + }, + "gironde-nzae": { + "area": 13032.87, + "population": 1331222 + }, + "gnagna-qjxm": { + "area": 15125.99, + "population": 1113493 + }, + "goias-suuo": { + "area": 348995.61, + "population": 8587177 + }, + "golestan-cond": { + "area": 20709.97, + "population": 1048209 + }, + "gombe-ogwc": { + "area": 18331.57, + "population": 3990366 + }, + "gomel-evel": { + "area": 40721.07, + "population": 1862369 + }, + "gorgol-abfb": { + "area": 13824.57, + "population": 59936 + }, + "gorj-qeiw": { + "area": 24826.07, + "population": 2024730 + }, + "gorno-altay-cstw": { + "area": 92890.37, + "population": 784826 + }, + "gorno-badakhshan-sgex": { + "area": 63555.32, + "population": 4169193 + }, + "gorontalo-gihp": { + "area": 17143.34, + "population": 2338680 + }, + "gotland-qouw": { + "area": 2924.05, + "population": 66657 + }, + "gourma-okzn": { + "area": 16488.06, + "population": 1213761 + }, + "govi-altay-ovst": { + "area": 142204.68, + "population": 293633 + }, + "gracias-a-dios-kumu": { + "area": 17642.08, + "population": 1488696 + }, + "granada-yhir": { + "area": 23048.45, + "population": 2085017 + }, + "grand-bassa-vzwu": { + "area": 20172.49, + "population": 968822 + }, + "grand-cape-mount-acht": { + "area": 11690.1, + "population": 561440 + }, + "grand-gedeh-cewx": { + "area": 20470.28, + "population": 983124 + }, + "granma-yeei": { + "area": 16039.25, + "population": 1513732 + }, + "greater-poland-ausi": { + "area": 29120.15, + "population": 3495196 + }, + "grodno-sbey": { + "area": 25090.49, + "population": 1147508 + }, + "grosseto-gtuw": { + "area": 14955.63, + "population": 2810397 + }, + "guadalajara-izim": { + "area": 12424.41, + "population": 1123941 + }, + "guadeloupe-wqci": { + "area": 1484.63, + "population": 151645 + }, + "guainia-utmi": { + "area": 71159.88, + "population": 3107997 + }, + "guanacaste-ffmq": { + "area": 11471.08, + "population": 1065816 + }, + "guanajuato-uvcd": { + "area": 30659.76, + "population": 1957098 + }, + "guangdong-eajt": { + "area": 181501.72, + "population": 26925346 + }, + "guangxi-cgjb": { + "area": 238197.27, + "population": 35335995 + }, + "guarico-meqi": { + "area": 65773.01, + "population": 2015336 + }, + "guaviare-spmp": { + "area": 55477.1, + "population": 2423032 + }, + "guayas-whue": { + "area": 20555.8, + "population": 1373024 + }, + "guelmim-es-semara-qzhx": { + "area": 101769.56, + "population": 6117262 + }, + "guera-qyrk": { + "area": 62029.89, + "population": 776999 + }, + "guerrero-iroj": { + "area": 66334.06, + "population": 4234288 + }, + "guidimaka-cqhe": { + "area": 10953.83, + "population": 47490 + }, + "guizhou-qjjd": { + "area": 175660.78, + "population": 26058857 + }, + "gujarat-sooi": { + "area": 194770.34, + "population": 83663723 + }, + "gulbene-gcky": { + "area": 15579.71, + "population": 457605 + }, + "gulf-yenl": { + "area": 35820.02, + "population": 640865 + }, + "gumushane-melw": { + "area": 14866.32, + "population": 1536636 + }, + "guyane-francaise-gmcw": { + "area": 84492.27, + "population": 8630326 + }, + "gyeonggi-asoh": { + "area": 12793.56, + "population": 6290941 + }, + "ha-il-eoqk": { + "area": 121928.33, + "population": 2159019 + }, + "ha-noi-shnb": { + "area": 21505.92, + "population": 6085167 + }, + "hadarom-uves": { + "area": 22786.63, + "population": 9053300 + }, + "hadjer-lamis-avbt": { + "area": 30948.84, + "population": 387672 + }, + "hadramawt-itbu": { + "area": 168717.06, + "population": 10641314 + }, + "hainan-mpfj": { + "area": 39879.26, + "population": 5915994 + }, + "hajdu-bihar-edea": { + "area": 19110.97, + "population": 1998035 + }, + "hajjah-uqbs": { + "area": 17420.41, + "population": 1098739 + }, + "hamadan-akna": { + "area": 20502.23, + "population": 1037695 + }, + "hamah-xsli": { + "area": 10681.32, + "population": 970614 + }, + "hamgyong-bukto-yzdb": { + "area": 18411.25, + "population": 3783888 + }, + "hamgyong-namdo-cxda": { + "area": 18886.39, + "population": 3881538 + }, + "hardap-qqnp": { + "area": 112261.84, + "population": 336800 + }, + "harghita-ybxj": { + "area": 15845.12, + "population": 1292274 + }, + "harju-qnwd": { + "area": 23101.83, + "population": 651158 + }, + "haryana-wkdb": { + "area": 45704.27, + "population": 19632299 + }, + "hasaka-al-haksa-vlle": { + "area": 22373.65, + "population": 2033098 + }, + "haut-mbomou-qdrb": { + "area": 56171.05, + "population": 427883 + }, + "haut-ogooue-kcwn": { + "area": 33885.28, + "population": 271409 + }, + "haut-sassandra-aiql": { + "area": 24340.12, + "population": 1909198 + }, + "haute-corse-gmyy": { + "area": 9957.73, + "population": 1017116 + }, + "haute-garonne-bwac": { + "area": 11890.8, + "population": 1214566 + }, + "haute-kotto-ymrh": { + "area": 86131.17, + "population": 656104 + }, + "haute-matsiatra-iovl": { + "area": 22072.89, + "population": 985331 + }, + "hawaii-mxvl": { + "area": 17870.13, + "population": 613248 + }, + "hawke-s-bay-iunb": { + "area": 16277.3, + "population": 277627 + }, + "hebei-owfx": { + "area": 189022.91, + "population": 28041097 + }, + "hedmark-akyb": { + "area": 27135.66, + "population": 335768 + }, + "heilongjiang-shfe": { + "area": 452654.76, + "population": 67150251 + }, + "henan-qdfb": { + "area": 166149, + "population": 24647806 + }, + "hentiy-kufs": { + "area": 81309.44, + "population": 167893 + }, + "hessen-bqlm": { + "area": 21029.97, + "population": 4837906 + }, + "hidalgo-drfz": { + "area": 22065.41, + "population": 1408497 + }, + "highland-saxg": { + "area": 31645.95, + "population": 7933936 + }, + "hiiraan-knsf": { + "area": 34494.15, + "population": 721143 + }, + "hilmand-gmdm": { + "area": 57134, + "population": 3363333 + }, + "himachal-pradesh-ggdz": { + "area": 55261.37, + "population": 23737557 + }, + "hirat-yddg": { + "area": 55765.16, + "population": 3282753 + }, + "hiroshima-tqmi": { + "area": 15610.88, + "population": 4629819 + }, + "hodh-ech-chargui-twfu": { + "area": 184607.78, + "population": 800363 + }, + "hodh-el-gharbi-oyfv": { + "area": 50407.35, + "population": 218540 + }, + "hoima-dcsv": { + "area": 15888.25, + "population": 2873991 + }, + "hokkaido-ksfx": { + "area": 91810.14, + "population": 27228724 + }, + "homs-hims-vcmm": { + "area": 49887.5, + "population": 4533286 + }, + "hordaland-yrai": { + "area": 17311.03, + "population": 214201 + }, + "hormozgan-einz": { + "area": 68890.99, + "population": 3486830 + }, + "houaphan-lmpf": { + "area": 17443.08, + "population": 541146 + }, + "houet-yrsy": { + "area": 11534.53, + "population": 849109 + }, + "hovd-wxnd": { + "area": 75653.84, + "population": 156215 + }, + "hovsgol-munb": { + "area": 100297.77, + "population": 207101 + }, + "hualien-babs": { + "area": 13132.13, + "population": 8398928 + }, + "huambo-czeb": { + "area": 33161.68, + "population": 831567 + }, + "huancavelica-orqp": { + "area": 22069.05, + "population": 545202 + }, + "huanuco-uwqc": { + "area": 37040.72, + "population": 915067 + }, + "hubei-eefc": { + "area": 186529.38, + "population": 27671188 + }, + "huehuetenango-mjgd": { + "area": 14524.08, + "population": 2185014 + }, + "huelva-qhbd": { + "area": 11679.32, + "population": 1056539 + }, + "huesca-ihkb": { + "area": 15541.79, + "population": 1405946 + }, + "huila-appk": { + "area": 19338.36, + "population": 844627 + }, + "huila-ylej": { + "area": 79627.9, + "population": 1996762 + }, + "hunan-gkfg": { + "area": 211524.13, + "population": 31379099 + }, + "hwanghae-bukto-oudy": { + "area": 18971.85, + "population": 3899102 + }, + "hyogo-ksja": { + "area": 19776.59, + "population": 5865269 + }, + "iasi-gzad": { + "area": 25447.38, + "population": 2075402 + }, + "ibaraki-odfe": { + "area": 16187.34, + "population": 4800782 + }, + "ica-hijd": { + "area": 22348.78, + "population": 552112 + }, + "ida-viru-ictd": { + "area": 13434.18, + "population": 378661 + }, + "idaho-slym": { + "area": 216036.57, + "population": 7413715 + }, + "idlib-yoou": { + "area": 10248.27, + "population": 931262 + }, + "ihorombe-mmvk": { + "area": 24921.73, + "population": 1112504 + }, + "ilam-oqih": { + "area": 18987.81, + "population": 961044 + }, + "iles-eparses-de-l-ocean-indien-zmrd": { + "area": 4.41, + "population": 0 + }, + "iles-saint-paul-et-nouvelle-amsterdam-smio": { + "area": 15.22, + "population": 0 + }, + "illinois-syye": { + "area": 149994.47, + "population": 5147352 + }, + "illizi-ccdj": { + "area": 196199.9, + "population": 3635539 + }, + "iloilo-xbzy": { + "area": 14536.18, + "population": 4784475 + }, + "imereti-vlst": { + "area": 17703.46, + "population": 937007 + }, + "inagua-ttnr": { + "area": 1656.27, + "population": 32073 + }, + "inchiri-othg": { + "area": 44851.89, + "population": 194455 + }, + "indiana-ivyc": { + "area": 93617.91, + "population": 3212680 + }, + "inhambane-aqfs": { + "area": 69768.5, + "population": 2656269 + }, + "inner-mongol-ioap": { + "area": 1142090.35, + "population": 169426370 + }, + "iowa-cdyh": { + "area": 144565.1, + "population": 4961032 + }, + "iringa-ykrf": { + "area": 39024.95, + "population": 2385476 + }, + "irkutsk-wjtq": { + "area": 769101.69, + "population": 6498105 + }, + "isere-ghaj": { + "area": 14775.05, + "population": 1509173 + }, + "isles-of-scilly-oznv": { + "area": 138.27, + "population": 34666 + }, + "itapua-jomj": { + "area": 15542.22, + "population": 271347 + }, + "ivano-frankivs-k-iocz": { + "area": 21905.89, + "population": 1691855 + }, + "ivanovo-uneq": { + "area": 23477.15, + "population": 198357 + }, + "iwate-uvfz": { + "area": 15287.41, + "population": 4533885 + }, + "izmir-ehof": { + "area": 18082.37, + "population": 1869059 + }, + "jaen-ekbf": { + "area": 13900.24, + "population": 1257449 + }, + "jalal-abad-colt": { + "area": 34098.21, + "population": 1095213 + }, + "jalisco-kpbu": { + "area": 88943.41, + "population": 5677507 + }, + "jambi-sxbt": { + "area": 51911.19, + "population": 7081683 + }, + "jammu-and-kashmir-vhis": { + "area": 43100.27, + "population": 18513746 + }, + "jamtland-kquh": { + "area": 53751.55, + "population": 1225335 + }, + "janub-sina-mrqx": { + "area": 34788.29, + "population": 3443197 + }, + "jawa-barat-msbq": { + "area": 38425.89, + "population": 5242028 + }, + "jawa-tengah-eqby": { + "area": 39391.75, + "population": 5373790 + }, + "jawa-timur-skbb": { + "area": 58679.14, + "population": 8004961 + }, + "jawzjan-ibdk": { + "area": 11188.27, + "population": 658625 + }, + "jeju-afop": { + "area": 1884.92, + "population": 926865 + }, + "jharkhand-qjit": { + "area": 78360.13, + "population": 33659643 + }, + "jiangsu-kpkl": { + "area": 104271.27, + "population": 15468394 + }, + "jiangxi-geke": { + "area": 168570.04, + "population": 25006962 + }, + "jigawa-yzkn": { + "area": 24086.2, + "population": 5243018 + }, + "jilin-akky": { + "area": 190812.11, + "population": 28306520 + }, + "jinotega-yvws": { + "area": 16885.59, + "population": 841327 + }, + "jizan-gfqv": { + "area": 15832, + "population": 280342 + }, + "jizzakh-uoed": { + "area": 25776.41, + "population": 1932448 + }, + "johor-omus": { + "area": 22058.42, + "population": 2045571 + }, + "jonglei-setz": { + "area": 121902.09, + "population": 2135133 + }, + "jonkoping-eiuj": { + "area": 20396.92, + "population": 464974 + }, + "jubbada-dhexe-unxw": { + "area": 19146.82, + "population": 400288 + }, + "jubbada-hoose-wuxk": { + "area": 49609.45, + "population": 1037147 + }, + "jujuy-yinm": { + "area": 53502.47, + "population": 857714 + }, + "junin-rnyf": { + "area": 44382.04, + "population": 1096429 + }, + "jutiapa-wdml": { + "area": 17301, + "population": 2602776 + }, + "juzno-backi-girr": { + "area": 13935.53, + "population": 1243844 + }, + "k-maras-wiwf": { + "area": 23133.16, + "population": 2391128 + }, + "k-p-stsm": { + "area": 75314.96, + "population": 18511676 + }, + "kabardin-balkar-kgec": { + "area": 12828.58, + "population": 108388 + }, + "kabul-agcw": { + "area": 26807.45, + "population": 1578086 + }, + "kachin-etts": { + "area": 88190.35, + "population": 6960380 + }, + "kaduna-aymn": { + "area": 44562.41, + "population": 9700225 + }, + "kaffrine-ufni": { + "area": 11160.33, + "population": 895276 + }, + "kagera-umrg": { + "area": 30380.99, + "population": 1857098 + }, + "kainuu-ayke": { + "area": 24285.59, + "population": 396864 + }, + "kakheti-lswd": { + "area": 11005.23, + "population": 582483 + }, + "kalbacar-zzbg": { + "area": 19128.46, + "population": 2188614 + }, + "kalimantan-barat-qpei": { + "area": 158592.78, + "population": 21635099 + }, + "kalimantan-selatan-smex": { + "area": 43024.8, + "population": 5869408 + }, + "kalimantan-tengah-area": { + "area": 154991.93, + "population": 21143873 + }, + "kalimantan-timur-kxex": { + "area": 199167.68, + "population": 27170294 + }, + "kaliningrad-ihxa": { + "area": 14599.84, + "population": 123353 + }, + "kalmar-umuv": { + "area": 10948.47, + "population": 249585 + }, + "kalmyk-yjee": { + "area": 73495.49, + "population": 620960 + }, + "kaluga-fmjg": { + "area": 29392.84, + "population": 248339 + }, + "kamchatka-rncn": { + "area": 503555, + "population": 4254513 + }, + "kampong-spoe-gatq": { + "area": 16874.58, + "population": 1496085 + }, + "kampong-thum-lytp": { + "area": 18807.16, + "population": 1667427 + }, + "kanchanaburi-udyq": { + "area": 20175.77, + "population": 2661371 + }, + "kandahar-cacs": { + "area": 53804.77, + "population": 3167350 + }, + "kanem-bhvv": { + "area": 68464.34, + "population": 857598 + }, + "kangwon-do-kwdz": { + "area": 11565.06, + "population": 2376855 + }, + "kankan-kmei": { + "area": 22979.98, + "population": 1168034 + }, + "kano-oomx": { + "area": 20149.59, + "population": 4386107 + }, + "kansas-ondv": { + "area": 212768.04, + "population": 7301549 + }, + "kaoh-kong-astc": { + "area": 15265.2, + "population": 1353400 + }, + "kaohsiung-city-gnnm": { + "area": 11936.2, + "population": 7634050 + }, + "kara-khrl": { + "area": 20182.57, + "population": 2785856 + }, + "karachay-cherkess-gied": { + "area": 14290.03, + "population": 120736 + }, + "karakalpakstan-cuod": { + "area": 165249.37, + "population": 12388684 + }, + "karas-etva": { + "area": 160644.26, + "population": 481953 + }, + "karelia-ohem": { + "area": 177373.37, + "population": 1498619 + }, + "karnali-mbpa": { + "area": 21142.04, + "population": 4081798 + }, + "karnataka-rslx": { + "area": 196596.82, + "population": 84448285 + }, + "karnten-cypo": { + "area": 17144.56, + "population": 1793789 + }, + "kars-uiog": { + "area": 15698.61, + "population": 1622665 + }, + "kasai-occidental-udyu": { + "area": 154094.53, + "population": 5721392 + }, + "kasai-oriental-gzyi": { + "area": 169956.4, + "population": 6310330 + }, + "kasese-khsy": { + "area": 15483.12, + "population": 2800708 + }, + "kashkadarya-uvou": { + "area": 28905.07, + "population": 2167002 + }, + "kassala-odzh": { + "area": 51430.22, + "population": 1178436 + }, + "kastamonu-okoh": { + "area": 21609.77, + "population": 2233664 + }, + "kasungu-scgl": { + "area": 16262.28, + "population": 2526706 + }, + "katakwi-xlyx": { + "area": 17384.5, + "population": 3144644 + }, + "katanga-igym": { + "area": 488243.27, + "population": 18128037 + }, + "katavi-ytvx": { + "area": 48272.78, + "population": 2950768 + }, + "katsina-ezmx": { + "area": 24159.77, + "population": 5259032 + }, + "kauno-keod": { + "area": 28450.59, + "population": 1218784 + }, + "kavadartsi-yiil": { + "area": 25190.18, + "population": 2083459 + }, + "kavango-gglu": { + "area": 43358.98, + "population": 130083 + }, + "kayah-uutt": { + "area": 11450.32, + "population": 903710 + }, + "kayes-twwi": { + "area": 123365.34, + "population": 1926402 + }, + "kayin-owtu": { + "area": 29677.62, + "population": 2342291 + }, + "kayseri-kzoa": { + "area": 17484.85, + "population": 1807298 + }, + "kebbi-wamo": { + "area": 36603.37, + "population": 7967723 + }, + "kebili-ecss": { + "area": 30117.7, + "population": 2219348 + }, + "kedah-gout": { + "area": 12356.68, + "population": 1145887 + }, + "kedougou-syne": { + "area": 19164.76, + "population": 1537388 + }, + "kelantan-ypuu": { + "area": 15190.74, + "population": 1408702 + }, + "kemerovo-larh": { + "area": 95020.52, + "population": 802824 + }, + "kemo-uuzv": { + "area": 16532.24, + "population": 125934 + }, + "kenedougou-yxcn": { + "area": 11455.58, + "population": 843297 + }, + "kentriki-makedonia-wdye": { + "area": 23109.89, + "population": 1664466 + }, + "kentucky-abeb": { + "area": 104062.45, + "population": 3571105 + }, + "kerala-clls": { + "area": 39086.05, + "population": 16789438 + }, + "kermadec-islands-ocbs": { + "area": 18.74, + "population": 320 + }, + "kerman-oykw": { + "area": 182500.48, + "population": 9237030 + }, + "kermanshah-ccky": { + "area": 23642.5, + "population": 1196635 + }, + "kgalagadi-gaxy": { + "area": 105658.75, + "population": 418836 + }, + "khabarovsk-vetk": { + "area": 794971.98, + "population": 6716682 + }, + "khakass-cuel": { + "area": 59300.88, + "population": 501030 + }, + "khammouan-qmwz": { + "area": 16385.19, + "population": 508326 + }, + "khanty-mansiy-tkto": { + "area": 530407.43, + "population": 4481388 + }, + "kharkiv-mesc": { + "area": 31092.91, + "population": 2401396 + }, + "khartoum-wpzy": { + "area": 22072.18, + "population": 505746 + }, + "khatlon-qlhv": { + "area": 24342.25, + "population": 1596838 + }, + "khenchela-yomx": { + "area": 16332.06, + "population": 302629 + }, + "kherson-ybsa": { + "area": 26828.03, + "population": 2072006 + }, + "khmel-nyts-kyy-etsv": { + "area": 20885.77, + "population": 1613069 + }, + "khomas-ievh": { + "area": 36681.66, + "population": 110050 + }, + "khon-kaen-msqg": { + "area": 11093.7, + "population": 1463363 + }, + "khulna-purw": { + "area": 31139.6, + "population": 36534840 + }, + "khuzestan-yrkr": { + "area": 64966.45, + "population": 3288194 + }, + "kidal-yhwp": { + "area": 147113.5, + "population": 2297240 + }, + "kien-giang-qaqj": { + "area": 28922.74, + "population": 8183780 + }, + "kiev-yefc": { + "area": 28716.82, + "population": 2217883 + }, + "kigoma-aerb": { + "area": 47804.34, + "population": 2922134 + }, + "kilimanjaro-wgrc": { + "area": 13258.36, + "population": 810443 + }, + "kindia-gbod": { + "area": 18888.92, + "population": 960093 + }, + "kinmen-mdpx": { + "area": 142.67, + "population": 91248 + }, + "kinshasa-city-aoyb": { + "area": 11321.63, + "population": 420362 + }, + "kirov-gfvm": { + "area": 120115.68, + "population": 1014852 + }, + "kirovohrad-gwfx": { + "area": 24323.51, + "population": 1878575 + }, + "kirsehir-iywz": { + "area": 16659.4, + "population": 1721976 + }, + "kissidougou-hloa": { + "area": 13953.79, + "population": 709248 + }, + "kitgum-hetq": { + "area": 20601.37, + "population": 3726538 + }, + "klaipedos-kooa": { + "area": 21651.69, + "population": 927528 + }, + "kocevje-gyjj": { + "area": 19816.36, + "population": 2087946 + }, + "kochi-aimd": { + "area": 23048.69, + "population": 6835697 + }, + "kogi-gqmy": { + "area": 29089.17, + "population": 6332052 + }, + "kohgiluyeh-and-buyer-ahmad-yekj": { + "area": 13773.05, + "population": 697105 + }, + "kolda-oanf": { + "area": 14868.05, + "population": 1192708 + }, + "komi-eoeh": { + "area": 414412.3, + "population": 3501350 + }, + "kommune-kujalleq-nbjz": { + "area": 56728.57, + "population": 1429 + }, + "kommuneqarfik-sermersooq-eqcb": { + "area": 549010.9, + "population": 13832 + }, + "komoe-siqa": { + "area": 15667.92, + "population": 1153387 + }, + "kompienga-wuin": { + "area": 12313.35, + "population": 906441 + }, + "kon-tum-ywew": { + "area": 15890.81, + "population": 4496354 + }, + "konya-ubwb": { + "area": 40247.82, + "population": 4160160 + }, + "kordestan-gakx": { + "area": 29851.38, + "population": 1510889 + }, + "kostroma-hmrp": { + "area": 59952.81, + "population": 506539 + }, + "kouilou-qfqd": { + "area": 15643.55, + "population": 240437 + }, + "koulikoro-oywj": { + "area": 91909.84, + "population": 1435211 + }, + "kouroussa-jsoo": { + "area": 16257.7, + "population": 826352 + }, + "kracheh-suom": { + "area": 12555.32, + "population": 1113144 + }, + "krasnodar-nzpm": { + "area": 86316.47, + "population": 729284 + }, + "krasnoyarsk-znif": { + "area": 2338285.38, + "population": 19756065 + }, + "kronoberg-ykuk": { + "area": 12467.01, + "population": 284201 + }, + "kunene-cmvm": { + "area": 116415.36, + "population": 349261 + }, + "kurgan-yyxu": { + "area": 72116.01, + "population": 609305 + }, + "kursk-obra": { + "area": 30492.96, + "population": 257634 + }, + "kusini-pemba-iltp": { + "area": 845.41, + "population": 51677 + }, + "kutahya-odwc": { + "area": 16675.66, + "population": 1723656 + }, + "kuyavian-pomeranian-gfwe": { + "area": 18108.41, + "population": 2173492 + }, + "kwara-kema": { + "area": 35669.3, + "population": 7764396 + }, + "kwazulu-natal-capj": { + "area": 95181.86, + "population": 4530076 + }, + "kweneng-kxwi": { + "area": 46589.56, + "population": 184683 + }, + "l-artibonite-egma": { + "area": 14884.4, + "population": 5746428 + }, + "l-viv-wbox": { + "area": 21683.2, + "population": 1674656 + }, + "la-altagracia-iexf": { + "area": 9479.1, + "population": 2029857 + }, + "la-araucania-aepm": { + "area": 33988.46, + "population": 761528 + }, + "la-guajira-utww": { + "area": 20929.88, + "population": 914139 + }, + "la-libertad-sqmm": { + "area": 32780.07, + "population": 809810 + }, + "la-pampa-aznx": { + "area": 143100.85, + "population": 2294094 + }, + "la-paz-kftj": { + "area": 134061.68, + "population": 1413924 + }, + "la-reunion-voeo": { + "area": 2555.12, + "population": 260989 + }, + "la-rioja-anut": { + "area": 23280.41, + "population": 2106000 + }, + "la-rioja-opnr": { + "area": 90723.75, + "population": 1454420 + }, + "laayoune-boujdour-sakia-el-hamra-cyhw": { + "area": 75698.77, + "population": 4550174 + }, + "lac-rmsk": { + "area": 20316.72, + "population": 254492 + }, + "ladakh-ppmi": { + "area": 64311.44, + "population": 27625019 + }, + "laghouat-mwwa": { + "area": 27757.38, + "population": 514338 + }, + "lagunes-igqk": { + "area": 13893.25, + "population": 1089763 + }, + "lahij-saqb": { + "area": 13805.13, + "population": 870717 + }, + "lakes-uvxp": { + "area": 38495.39, + "population": 674253 + }, + "lam-djong-iuhi": { + "area": 32899.1, + "population": 9308901 + }, + "lambayeque-yoml": { + "area": 13742.68, + "population": 339504 + }, + "lampang-avtp": { + "area": 17007.54, + "population": 2243452 + }, + "lampung-yjht": { + "area": 36572.66, + "population": 4989213 + }, + "landes-kudi": { + "area": 17436.21, + "population": 1780993 + }, + "lang-son-yfzb": { + "area": 17946.9, + "population": 5078131 + }, + "lao-cai-suzu": { + "area": 14843.41, + "population": 4199988 + }, + "lapland-gcno": { + "area": 97422.41, + "population": 1592035 + }, + "lara-ggqj": { + "area": 27318.81, + "population": 837069 + }, + "las-palmas-ujic": { + "area": 3988.46, + "population": 360806 + }, + "lavalleja-mdrw": { + "area": 15096.45, + "population": 292246 + }, + "le-kef-oxic": { + "area": 13672.44, + "population": 1007511 + }, + "lekoumou-inqi": { + "area": 21156.83, + "population": 325174 + }, + "leninabad-dnbu": { + "area": 25388.17, + "population": 1665450 + }, + "leningrad-mknu": { + "area": 87335.38, + "population": 737893 + }, + "leon-gvhg": { + "area": 14389.39, + "population": 716953 + }, + "leon-uyuj": { + "area": 15916.01, + "population": 1439800 + }, + "lerida-onbh": { + "area": 18695.48, + "population": 1691237 + }, + "lesser-poland-ukhv": { + "area": 15581.93, + "population": 1870248 + }, + "liaoning-wswa": { + "area": 150005.27, + "population": 22252923 + }, + "libertador-general-bernardo-o-higgins-ghiu": { + "area": 18297.09, + "population": 409955 + }, + "licko-senjska-cvjk": { + "area": 13774.51, + "population": 981719 + }, + "likouala-aiqf": { + "area": 67816.31, + "population": 1042316 + }, + "lilongwe-kdpi": { + "area": 14922.11, + "population": 2318480 + }, + "lima-iioy": { + "area": 39624.29, + "population": 978892 + }, + "limon-ynmv": { + "area": 18418.51, + "population": 1711324 + }, + "limpopo-kvkx": { + "area": 126340.77, + "population": 6013050 + }, + "lincolnshire-odkr": { + "area": 14009.94, + "population": 3512425 + }, + "lindi-yitn": { + "area": 66535.73, + "population": 4067127 + }, + "lipetsk-qvwo": { + "area": 24153.52, + "population": 204072 + }, + "littoral-oyts": { + "area": 26931.24, + "population": 1472237 + }, + "lobaye-uriq": { + "area": 19077.3, + "population": 145321 + }, + "lodz-gveo": { + "area": 18280.67, + "population": 2194169 + }, + "loei-avqi": { + "area": 10522.22, + "population": 1387979 + }, + "logone-oriental-wotz": { + "area": 32474.4, + "population": 406781 + }, + "loir-et-cher-fwdj": { + "area": 19060.35, + "population": 1946888 + }, + "loire-atlantique-mode": { + "area": 14750.49, + "population": 1506665 + }, + "loja-czuz": { + "area": 17507.52, + "population": 1169414 + }, + "long-an-irqd": { + "area": 15336.45, + "population": 4339496 + }, + "lord-howe-island-krnq": { + "area": 985.47, + "population": 3218 + }, + "lorestan-ydnw": { + "area": 29595.53, + "population": 1497940 + }, + "loreto-ipkx": { + "area": 374665.09, + "population": 9255856 + }, + "lori-aqmn": { + "area": 13464.18, + "population": 1329935 + }, + "los-lagos-mmix": { + "area": 54662.4, + "population": 1224738 + }, + "los-rios-gxix": { + "area": 16983.03, + "population": 380513 + }, + "los-rios-mouz": { + "area": 26555.51, + "population": 1773774 + }, + "lot-et-garonne-psdh": { + "area": 23627.7, + "population": 2413414 + }, + "louang-namtha-fbyj": { + "area": 16729.18, + "population": 518998 + }, + "louangphrabang-quyo": { + "area": 20139.25, + "population": 624791 + }, + "louga-wgwg": { + "area": 25333.72, + "population": 2032259 + }, + "louisiana-wlgr": { + "area": 125901.91, + "population": 4320569 + }, + "lower-silesian-eayz": { + "area": 19841.94, + "population": 2381563 + }, + "luapula-imtp": { + "area": 49880.99, + "population": 1178132 + }, + "lublin-gyez": { + "area": 24758.89, + "population": 2971727 + }, + "lubombo-ybpl": { + "area": 16928.43, + "population": 1148130 + }, + "lubusz-wsem": { + "area": 14065.08, + "population": 1688185 + }, + "lugo-qbvz": { + "area": 18579.84, + "population": 1680775 + }, + "luhans-k-whxu": { + "area": 26380.04, + "population": 2037407 + }, + "lunda-norte-kkid": { + "area": 106426.96, + "population": 2668779 + }, + "lunda-sul-iyot": { + "area": 77942.23, + "population": 1954491 + }, + "lusaka-ygtc": { + "area": 21965.59, + "population": 518802 + }, + "luxembourg-krjs": { + "area": 12093.27, + "population": 4504525 + }, + "m-sila-sobh": { + "area": 17612.46, + "population": 326355 + }, + "ma-an-lrvp": { + "area": 39509.98, + "population": 4487477 + }, + "ma-rib-oyto": { + "area": 17278.98, + "population": 1089819 + }, + "machinga-mtss": { + "area": 10574.98, + "population": 1643058 + }, + "macquarie-island-yijx": { + "area": 50.24, + "population": 164 + }, + "madang-elun": { + "area": 31592.22, + "population": 565225 + }, + "madeira-croh": { + "area": 684.06, + "population": 72055 + }, + "madhya-pradesh-qlpo": { + "area": 310323.16, + "population": 133299508 + }, + "madre-de-dios-fpxm": { + "area": 83184.66, + "population": 2055023 + }, + "madrid-kpbi": { + "area": 23060.8, + "population": 2086134 + }, + "mae-hong-son-qftf": { + "area": 13687.98, + "population": 1805572 + }, + "maekel-litf": { + "area": 11221.94, + "population": 545875 + }, + "mafraq-invm": { + "area": 26474.22, + "population": 3006898 + }, + "maga-buryatdan-wvtk": { + "area": 481454.84, + "population": 4067790 + }, + "magallanes-y-antartica-chilena-eqmb": { + "area": 197175.92, + "population": 4417822 + }, + "magdalena-wrsg": { + "area": 28238.36, + "population": 1233346 + }, + "magway-karn": { + "area": 47041.44, + "population": 3712722 + }, + "mahaica-berbice-igzl": { + "area": 36306.78, + "population": 133510 + }, + "maharashtra-qypg": { + "area": 308473.18, + "population": 132504847 + }, + "maine-maiz": { + "area": 86901.66, + "population": 2982199 + }, + "makira-qcnj": { + "area": 6076.08, + "population": 123269 + }, + "makkah-yuqo": { + "area": 139143.19, + "population": 2463848 + }, + "malanje-quaz": { + "area": 85903.28, + "population": 2154124 + }, + "malatya-kfwd": { + "area": 19662.38, + "population": 2032375 + }, + "mali-ayzp": { + "area": 18396.54, + "population": 935066 + }, + "maluku-qzjd": { + "area": 44766.15, + "population": 6106962 + }, + "maluku-utara-osjp": { + "area": 40121, + "population": 5473274 + }, + "mambere-kadei-carz": { + "area": 30111.84, + "population": 229377 + }, + "mamou-rzzq": { + "area": 13746.83, + "population": 698729 + }, + "manabi-myuy": { + "area": 20845.16, + "population": 1392352 + }, + "manawatu-wanganui-zrxu": { + "area": 40293.76, + "population": 687255 + }, + "mandalay-gcro": { + "area": 36751.25, + "population": 2900575 + }, + "mandiana-alzh": { + "area": 11156.75, + "population": 567079 + }, + "mandoul-jypx": { + "area": 17200.8, + "population": 215461 + }, + "mangghystau-ooqu": { + "area": 166748.27, + "population": 1137654 + }, + "mangochi-qesd": { + "area": 22504.46, + "population": 3496567 + }, + "manica-wffl": { + "area": 64363.39, + "population": 2450483 + }, + "manicaland-czzx": { + "area": 34540.2, + "population": 1293263 + }, + "maniema-eoks": { + "area": 128818.77, + "population": 4782926 + }, + "manipur-ahpm": { + "area": 21593.89, + "population": 9275669 + }, + "manisa-cgwe": { + "area": 13122.51, + "population": 1356390 + }, + "manitoba-thmy": { + "area": 658964.26, + "population": 2433673 + }, + "manus-oxpg": { + "area": 1564.89, + "population": 27998 + }, + "manyara-mkvr": { + "area": 45617.66, + "population": 2788469 + }, + "maputo-gvfv": { + "area": 24565.4, + "population": 935269 + }, + "mara-cgtm": { + "area": 28849.32, + "population": 1763471 + }, + "maradi-knfe": { + "area": 39928.61, + "population": 784285 + }, + "maramures-iwjv": { + "area": 11180.69, + "population": 911859 + }, + "maranhao-ojve": { + "area": 331739.47, + "population": 8162583 + }, + "mariy-el-umod": { + "area": 23430.82, + "population": 197966 + }, + "markazi-qhny": { + "area": 28425.04, + "population": 1438697 + }, + "marlborough-district-oyxs": { + "area": 23248.96, + "population": 396537 + }, + "marne-hpgs": { + "area": 14396.43, + "population": 1470500 + }, + "marrakech-tensift-al-haouz-weha": { + "area": 32126.1, + "population": 1931066 + }, + "martinique-tuau": { + "area": 978.19, + "population": 99916 + }, + "mary-qxca": { + "area": 85529.15, + "population": 1066750 + }, + "maryland-ecia": { + "area": 34086.34, + "population": 1169739 + }, + "maseru-quuq": { + "area": 14749.02, + "population": 1033117 + }, + "mashonaland-central-mczz": { + "area": 27778.24, + "population": 1040080 + }, + "mashonaland-east-afzb": { + "area": 33714.01, + "population": 1262329 + }, + "mashonaland-west-ojzn": { + "area": 57749.82, + "population": 2162284 + }, + "masovian-uuhs": { + "area": 34720.3, + "population": 4167363 + }, + "massachusetts-ggid": { + "area": 26367.13, + "population": 904839 + }, + "masvingo-wizm": { + "area": 56205.6, + "population": 2104465 + }, + "matabeleland-north-otzk": { + "area": 76546.42, + "population": 2866071 + }, + "matabeleland-south-qdzj": { + "area": 55317.57, + "population": 2071215 + }, + "matagalpa-iykc": { + "area": 13511.26, + "population": 673201 + }, + "matam-qxzf": { + "area": 30159.99, + "population": 2419421 + }, + "matanzas-kobf": { + "area": 19443.76, + "population": 1835037 + }, + "mato-grosso-aqvz": { + "area": 906472.77, + "population": 22304127 + }, + "mato-grosso-do-sul-wfvs": { + "area": 357215.04, + "population": 8789419 + }, + "matruh-cafp": { + "area": 160516.75, + "population": 15887267 + }, + "maule-kvme": { + "area": 29012.32, + "population": 650035 + }, + "mayaguana-kkei": { + "area": 227.08, + "population": 4397 + }, + "mayo-kebbi-est-hepb": { + "area": 18140.18, + "population": 227228 + }, + "mayo-kebbi-ouest-tupl": { + "area": 13621.7, + "population": 170628 + }, + "mayotte-gthm": { + "area": 388.49, + "population": 39681 + }, + "maysan-rzfn": { + "area": 16357.45, + "population": 1463901 + }, + "mazandaran-ufnx": { + "area": 27916.86, + "population": 1412976 + }, + "mbarara-mcue": { + "area": 17425.61, + "population": 3152082 + }, + "mbarara-yqvb": { + "area": 14253.34, + "population": 2578257 + }, + "mbeya-getl": { + "area": 59577.87, + "population": 3641814 + }, + "mbomou-ydky": { + "area": 60472.83, + "population": 460652 + }, + "mecklenburg-vorpommern-pqcp": { + "area": 23512.91, + "population": 5409100 + }, + "medea-elbf": { + "area": 27810.93, + "population": 515330 + }, + "meghalaya-oepk": { + "area": 23876.41, + "population": 10256126 + }, + "meknes-tafilalet-yler": { + "area": 58814.76, + "population": 3535294 + }, + "melaky-ynaf": { + "area": 42348.01, + "population": 1890411 + }, + "melilla-uuxv": { + "area": 9.62, + "population": 870 + }, + "menabe-icfs": { + "area": 50896.89, + "population": 2272032 + }, + "mendoza-sany": { + "area": 150899.24, + "population": 2419112 + }, + "merida-yhqk": { + "area": 12244.28, + "population": 375174 + }, + "mersin-mqol": { + "area": 25415.2, + "population": 2627007 + }, + "meta-ymbx": { + "area": 86706.22, + "population": 3787003 + }, + "mexico-ewrz": { + "area": 104.4, + "population": 6664 + }, + "mexico-mpba": { + "area": 28484.77, + "population": 1818263 + }, + "michigan-gtil": { + "area": 248088.63, + "population": 8513643 + }, + "michoacan-uekn": { + "area": 59179.39, + "population": 3777586 + }, + "midlands-cmzf": { + "area": 49296.18, + "population": 1845761 + }, + "midtjylland-gjbs": { + "area": 24364.67, + "population": 2829462 + }, + "mie-iljm": { + "area": 17789.42, + "population": 5275924 + }, + "milne-bay-gubz": { + "area": 14631.31, + "population": 261773 + }, + "minas-gerais-ymvg": { + "area": 588226.76, + "population": 14473556 + }, + "mindoro-occidental-sjgh": { + "area": 10862.26, + "population": 3575232 + }, + "minnesota-qlig": { + "area": 224415.92, + "population": 7701268 + }, + "minsk-sirf": { + "area": 39657.97, + "population": 1813748 + }, + "miranda-ujql": { + "area": 17779.82, + "population": 544787 + }, + "misiones-ocnz": { + "area": 29525.7, + "population": 473336 + }, + "misratah-cbhm": { + "area": 50356.51, + "population": 209099 + }, + "mississippi-scir": { + "area": 124167.75, + "population": 4261057 + }, + "missouri-ykif": { + "area": 181352.61, + "population": 6223467 + }, + "miyazaki-stvh": { + "area": 24513.37, + "population": 7270088 + }, + "mizdah-uchd": { + "area": 88696.12, + "population": 368299 + }, + "mizoram-yapy": { + "area": 21680.43, + "population": 9312841 + }, + "mogilev-srqt": { + "area": 28626.49, + "population": 1309226 + }, + "mon-cmto": { + "area": 17283.29, + "population": 1364075 + }, + "monagas-kkqm": { + "area": 29558.78, + "population": 905704 + }, + "monaragala-wspd": { + "area": 11377.47, + "population": 3550899 + }, + "mondol-kiri-jvon": { + "area": 13355.96, + "population": 1184128 + }, + "montana-abiq": { + "area": 378515.09, + "population": 12989480 + }, + "mopti-vqwe": { + "area": 82435.03, + "population": 1287258 + }, + "moquegua-xwwn": { + "area": 17839.5, + "population": 440713 + }, + "moravskoslezsky-sbcc": { + "area": 14536.6, + "population": 1967204 + }, + "mordovia-abon": { + "area": 25898.72, + "population": 218817 + }, + "more-og-romsdal-aval": { + "area": 13776.34, + "population": 170464 + }, + "morobe-anuo": { + "area": 35389.44, + "population": 633161 + }, + "morogoro-wbtj": { + "area": 68887.2, + "population": 4210865 + }, + "morona-santiago-unuy": { + "area": 23745.87, + "population": 1586104 + }, + "moselle-rygy": { + "area": 15783.35, + "population": 1612164 + }, + "moskovskaya-chlr": { + "area": 46817.99, + "population": 395563 + }, + "mou-houn-azqo": { + "area": 20591.32, + "population": 1515820 + }, + "moxico-ievb": { + "area": 200224.4, + "population": 5020858 + }, + "moyen-chari-vbpz": { + "area": 42340.22, + "population": 530362 + }, + "moyen-ogooue-eewo": { + "area": 17885.49, + "population": 143256 + }, + "mpumalanga-yrnu": { + "area": 75620.8, + "population": 3599089 + }, + "mtskheta-mtianeti-dbcb": { + "area": 19185.56, + "population": 1015450 + }, + "mtwara-eati": { + "area": 18535.46, + "population": 1133016 + }, + "mubende-stqq": { + "area": 19939.96, + "population": 3606897 + }, + "muchinga-kova": { + "area": 96448.93, + "population": 2278013 + }, + "mudug-kpld": { + "area": 63673, + "population": 1331163 + }, + "mugla-mmwr": { + "area": 24235.58, + "population": 2505077 + }, + "mukdahan-ihqz": { + "area": 12362.57, + "population": 1630738 + }, + "mukono-khqz": { + "area": 22790.4, + "population": 4122506 + }, + "murcia-qjxo": { + "area": 18610.99, + "population": 1683594 + }, + "mures-yhjv": { + "area": 11903.47, + "population": 970807 + }, + "murmansk-etas": { + "area": 146568.39, + "population": 1238350 + }, + "murzuq-cohu": { + "area": 284194.33, + "population": 1180080 + }, + "musandam-ubnu": { + "area": 1861.8, + "population": 29256 + }, + "mwanza-iyth": { + "area": 26522.51, + "population": 1621240 + }, + "mykolayiv-obnj": { + "area": 24838.52, + "population": 1918351 + }, + "mzimba-svso": { + "area": 11290.44, + "population": 1754220 + }, + "n-zi-comoe-uesl": { + "area": 28569.7, + "population": 2240959 + }, + "naama-swmc": { + "area": 31032.49, + "population": 575025 + }, + "nagaland-yvrv": { + "area": 16866.48, + "population": 7245008 + }, + "nagano-kfji": { + "area": 41422.11, + "population": 12284823 + }, + "najran-wlss": { + "area": 91538.04, + "population": 1620890 + }, + "nakasongola-gjqa": { + "area": 13998.99, + "population": 2532247 + }, + "nakhon-ratchasima-kfip": { + "area": 20723.36, + "population": 2733604 + }, + "nakhon-sawan-ckvs": { + "area": 14316.72, + "population": 1888509 + }, + "nakhon-si-thammarat-utgo": { + "area": 23954.62, + "population": 3159837 + }, + "namibe-mitv": { + "area": 62503.36, + "population": 1567344 + }, + "nampula-qyfx": { + "area": 78872.07, + "population": 3002866 + }, + "nan-kntk": { + "area": 11839.46, + "population": 1561734 + }, + "nana-grebizi-mcza": { + "area": 19833.78, + "population": 151084 + }, + "nana-mambere-glnv": { + "area": 27755.05, + "population": 211424 + }, + "napak-yzyn": { + "area": 14573.61, + "population": 2636189 + }, + "narino-osue": { + "area": 31462.42, + "population": 1374161 + }, + "naryn-eilp": { + "area": 45564.97, + "population": 1463519 + }, + "nassarawa-mrzh": { + "area": 34222.35, + "population": 7449430 + }, + "nationalparken-menh": { + "area": 910086.87, + "population": 22929 + }, + "navoi-ahpf": { + "area": 110107.21, + "population": 8254696 + }, + "nayarit-chkm": { + "area": 28443.91, + "population": 1815654 + }, + "nebraska-wikv": { + "area": 201023.37, + "population": 6898507 + }, + "neembucu-cfml": { + "area": 21006.25, + "population": 366741 + }, + "negros-occidental-erlu": { + "area": 25893.12, + "population": 8522529 + }, + "nenets-qyga": { + "area": 182575.24, + "population": 1542570 + }, + "neuquen-yvne": { + "area": 94770.04, + "population": 1519287 + }, + "nevada-uekc": { + "area": 287680.54, + "population": 9872316 + }, + "new-brunswick-wcni": { + "area": 85387.08, + "population": 315350 + }, + "new-earth-eikt": { + "area": 15721.27, + "population": 132828 + }, + "new-hampshire-koki": { + "area": 24726.86, + "population": 848550 + }, + "new-jersey-wlkg": { + "area": 20120.09, + "population": 690460 + }, + "new-mexico-awkn": { + "area": 316369.69, + "population": 10856840 + }, + "new-south-wales-guah": { + "area": 809082.46, + "population": 2642174 + }, + "new-taipei-city-wbtm": { + "area": 11556.25, + "population": 7391046 + }, + "new-york-cqkt": { + "area": 137896.66, + "population": 4732192 + }, + "newfoundland-and-labrador-kmno": { + "area": 429163.27, + "population": 1584977 + }, + "nghe-an-ifec": { + "area": 17310.87, + "population": 4898165 + }, + "ngounie-atwh": { + "area": 37919.21, + "population": 303719 + }, + "niari-svpz": { + "area": 28080.54, + "population": 431589 + }, + "niassa-eefk": { + "area": 130843.48, + "population": 4981554 + }, + "nicosia-gzli": { + "area": 6408.22, + "population": 1198575 + }, + "niederosterreich-uapp": { + "area": 23376.55, + "population": 2445825 + }, + "niedersachsen-vxfc": { + "area": 48391.36, + "population": 11132341 + }, + "niger-qfzz": { + "area": 71061.58, + "population": 15468491 + }, + "niigata-idhx": { + "area": 12708.95, + "population": 3769174 + }, + "niksic-mcna": { + "area": 13628.14, + "population": 622137 + }, + "nimba-xaov": { + "area": 11429.2, + "population": 548909 + }, + "nimroz-eoll": { + "area": 42118.19, + "population": 2479390 + }, + "ninawa-bhig": { + "area": 37880.59, + "population": 3390103 + }, + "ningxia-kwbm": { + "area": 52102.74, + "population": 7729317 + }, + "nitriansky-muds": { + "area": 16714.92, + "population": 1880157 + }, + "nizhegorod-clce": { + "area": 74732.02, + "population": 631407 + }, + "njombe-qvvy": { + "area": 22463.29, + "population": 1373112 + }, + "nkhata-bay-sgvt": { + "area": 10482.34, + "population": 1628664 + }, + "noord-holland-otrk": { + "area": 20544.87, + "population": 8633751 + }, + "nord-gkzb": { + "area": 66634.84, + "population": 3642694 + }, + "nord-kivu-gxmp": { + "area": 59491.5, + "population": 2208866 + }, + "nord-ouest-yvzz": { + "area": 17448.85, + "population": 953868 + }, + "nord-trondelag-kyan": { + "area": 21233.19, + "population": 262733 + }, + "nord-wofd": { + "area": 26193.55, + "population": 287800 + }, + "nordland-gaao": { + "area": 41140.8, + "population": 509063 + }, + "nordrhein-westfalen-hafe": { + "area": 34318.41, + "population": 7894885 + }, + "nordurland-eystra-kbfi": { + "area": 24617.34, + "population": 78451 + }, + "nordurland-vestra-safh": { + "area": 16428.37, + "population": 52354 + }, + "norrbotten-gqvp": { + "area": 104312.01, + "population": 2377925 + }, + "norte-de-santander-svgs": { + "area": 22190.92, + "population": 969216 + }, + "north-andros-dagc": { + "area": 8666.79, + "population": 167828 + }, + "north-carolina-ozkp": { + "area": 133392.93, + "population": 4577638 + }, + "north-chungcheong-kvoj": { + "area": 18087.71, + "population": 8894216 + }, + "north-dakota-ehku": { + "area": 182372.2, + "population": 6258456 + }, + "north-darfur-mfpn": { + "area": 292956.27, + "population": 6712595 + }, + "north-eastern-ewzp": { + "area": 127081.78, + "population": 11325403 + }, + "north-eleuthera-nwgq": { + "area": 663.79, + "population": 12854 + }, + "north-gyeongsang-mpof": { + "area": 19391.11, + "population": 9535134 + }, + "north-karelia-mhnr": { + "area": 21724.21, + "population": 355008 + }, + "north-kazakhstan-kdzy": { + "area": 98284.44, + "population": 670554 + }, + "north-khorasan-yfqe": { + "area": 25030.53, + "population": 1266889 + }, + "north-kordufan-kmzw": { + "area": 243618.82, + "population": 5582111 + }, + "north-ossetia-arkb": { + "area": 11217.99, + "population": 94780 + }, + "north-solomons-yvrf": { + "area": 11456.9, + "population": 204978 + }, + "north-west-ispe": { + "area": 105545.86, + "population": 5023340 + }, + "north-west-qhdz": { + "area": 133863.42, + "population": 530640 + }, + "north-western-uptr": { + "area": 125670.43, + "population": 2968191 + }, + "north-yorkshire-qalk": { + "area": 19364.99, + "population": 4854986 + }, + "northern-areas-mevu": { + "area": 66920.63, + "population": 16448432 + }, + "northern-bahr-el-ghazal-civj": { + "area": 38766.94, + "population": 679009 + }, + "northern-cape-glpq": { + "area": 374746.56, + "population": 17835651 + }, + "northern-cyprus-mvnb": { + "area": 3852.3, + "population": 326000 + }, + "northern-eotq": { + "area": 69533.76, + "population": 1642308 + }, + "northern-ibne": { + "area": 69801.47, + "population": 8654060 + }, + "northern-ostrobothnia-eins": { + "area": 36795.65, + "population": 601298 + }, + "northern-qinm": { + "area": 364944.16, + "population": 8362075 + }, + "northern-savonia-aknt": { + "area": 19847.4, + "population": 324338 + }, + "northern-swud": { + "area": 25373.2, + "population": 453958 + }, + "northern-territory-ylns": { + "area": 1368456.43, + "population": 4468890 + }, + "northern-yiap": { + "area": 36396.12, + "population": 3732515 + }, + "northland-aqhd": { + "area": 21746.08, + "population": 370904 + }, + "northwest-territories-gynw": { + "area": 1381627.76, + "population": 5102599 + }, + "nova-scotia-ndnz": { + "area": 77721.69, + "population": 287040 + }, + "novgorod-cvlo": { + "area": 54908.23, + "population": 463917 + }, + "novosibirsk-mivi": { + "area": 177639.82, + "population": 1500871 + }, + "nuble-wmop": { + "area": 13748.36, + "population": 308039 + }, + "nueva-ecija-ilyq": { + "area": 13232.8, + "population": 4355477 + }, + "nuevo-leon-rfly": { + "area": 64970.67, + "population": 4147260 + }, + "nugaal-gznd": { + "area": 30068.52, + "population": 628620 + }, + "nunavut-banx": { + "area": 2196730.94, + "population": 8112921 + }, + "nuristan-ofkf": { + "area": 13813.88, + "population": 813188 + }, + "nusa-tenggara-barat-ywlq": { + "area": 22818.08, + "population": 3112824 + }, + "nusa-tenggara-timur-iflm": { + "area": 50766.99, + "population": 6925592 + }, + "nwoya-payo": { + "area": 20178.62, + "population": 3650068 + }, + "nyanga-suwi": { + "area": 20334.32, + "population": 162870 + }, + "nyanza-wpnv": { + "area": 25177.36, + "population": 2243781 + }, + "oaxaca-vcjz": { + "area": 94038, + "population": 6002708 + }, + "oberosterreich-mbpq": { + "area": 11863.73, + "population": 1241269 + }, + "odessa-kpqz": { + "area": 34305.11, + "population": 2649483 + }, + "odisha-uxvk": { + "area": 157314.9, + "population": 67574712 + }, + "ogooue-ivindo-mwwj": { + "area": 42792.36, + "population": 342751 + }, + "ogooue-lolo-cxwk": { + "area": 29353.39, + "population": 235110 + }, + "ogooue-maritime-ulwt": { + "area": 27004.84, + "population": 216299 + }, + "ogun-soba": { + "area": 19999.77, + "population": 4353496 + }, + "ohangwena-ialq": { + "area": 10929.48, + "population": 32790 + }, + "ohio-kgmu": { + "area": 117067.22, + "population": 4017389 + }, + "oio-eyrz": { + "area": 10886.11, + "population": 561159 + }, + "oita-asvg": { + "area": 15735.7, + "population": 4666839 + }, + "okayama-mymn": { + "area": 12237.61, + "population": 3629387 + }, + "okinawa-cwvj": { + "area": 2851.53, + "population": 845697 + }, + "oklahoma-cimv": { + "area": 180773.42, + "population": 6203591 + }, + "olancho-qasq": { + "area": 32888.64, + "population": 2775250 + }, + "omagh-gocy": { + "area": 14406.19, + "population": 3611767 + }, + "omaheke-ablr": { + "area": 83454.21, + "population": 250373 + }, + "ombella-m-poko-iklq": { + "area": 31843.66, + "population": 242569 + }, + "omnogovi-axqk": { + "area": 165683.28, + "population": 342113 + }, + "omsk-aufy": { + "area": 140998.54, + "population": 1191290 + }, + "omusati-gtlm": { + "area": 35880.43, + "population": 107646 + }, + "ondo-aabr": { + "area": 15020.16, + "population": 3269548 + }, + "ontario-bzqu": { + "area": 1078730.93, + "population": 3983947 + }, + "oppland-slyc": { + "area": 24579.64, + "population": 304140 + }, + "oregon-qznu": { + "area": 252420.95, + "population": 8662315 + }, + "orel-svfw": { + "area": 24723.96, + "population": 208892 + }, + "orellana-cmuh": { + "area": 21408.17, + "population": 1429957 + }, + "orenburg-nkfp": { + "area": 124782.92, + "population": 1054285 + }, + "orense-aqch": { + "area": 12933.58, + "population": 1170002 + }, + "oriental-kiep": { + "area": 58955.22, + "population": 3543737 + }, + "orientale-scpz": { + "area": 502117.06, + "population": 18643159 + }, + "orne-jeiv": { + "area": 18684.2, + "population": 1908468 + }, + "oromiya-otzl": { + "area": 329340.31, + "population": 32532460 + }, + "oruro-qktm": { + "area": 55154.94, + "population": 581709 + }, + "osh-vjlq": { + "area": 29140.97, + "population": 935990 + }, + "oshikoto-cvln": { + "area": 38520.07, + "population": 115565 + }, + "osjecko-baranjska-ivlu": { + "area": 12304.15, + "population": 876926 + }, + "ostergotland-ohui": { + "area": 11858.03, + "population": 270319 + }, + "osun-qhbm": { + "area": 14942.92, + "population": 3252735 + }, + "otago-ctsh": { + "area": 32965.31, + "population": 562260 + }, + "otjozondjupa-yuld": { + "area": 104455.13, + "population": 313379 + }, + "ouaddai-ukka": { + "area": 31294.1, + "population": 391996 + }, + "ouaka-mmrt": { + "area": 49663.45, + "population": 378312 + }, + "ouargla-wxdg": { + "area": 211546.45, + "population": 3919907 + }, + "oudalan-ooyt": { + "area": 17001.83, + "population": 1251581 + }, + "oudomxai-trgc": { + "area": 11842.95, + "population": 367410 + }, + "oued-el-dahab-gwhv": { + "area": 78131.21, + "population": 4696385 + }, + "ouest-kdbx": { + "area": 14424.27, + "population": 788524 + }, + "ouest-wjof": { + "area": 14289.22, + "population": 5516649 + }, + "ouham-mnot": { + "area": 53831.37, + "population": 410061 + }, + "ouham-pende-obpq": { + "area": 31935.22, + "population": 243267 + }, + "ovorhangay-onqe": { + "area": 63011.27, + "population": 130109 + }, + "oxfordshire-qxcr": { + "area": 15587.78, + "population": 3908003 + }, + "oyo-ksbc": { + "area": 27879.93, + "population": 6068827 + }, + "p-yongan-bukto-ipdv": { + "area": 12459.38, + "population": 2560656 + }, + "p-yongan-namdo-uodu": { + "area": 15267.54, + "population": 3137791 + }, + "pahang-ihup": { + "area": 36343.98, + "population": 3370331 + }, + "paijat-hame-olnu": { + "area": 12093.35, + "population": 197624 + }, + "paktika-wbrf": { + "area": 18917.15, + "population": 1113604 + }, + "paktya-opvd": { + "area": 13985.01, + "population": 823262 + }, + "palawan-imjh": { + "area": 16783.43, + "population": 5524141 + }, + "palencia-qubl": { + "area": 16004.53, + "population": 1447807 + }, + "palermo-cprw": { + "area": 12021.85, + "population": 2259093 + }, + "panama-oriu": { + "area": 20096.49, + "population": 1087437 + }, + "pando-uitl": { + "area": 63737.21, + "population": 672225 + }, + "panevezio-cpal": { + "area": 14952.2, + "population": 640532 + }, + "pangasinan-cmhx": { + "area": 13997.91, + "population": 4607310 + }, + "papua-barat-mady": { + "area": 104271.88, + "population": 14224686 + }, + "papua-rydx": { + "area": 323629.23, + "population": 44149237 + }, + "para-mmiu": { + "area": 1261426.4, + "population": 31037903 + }, + "para-scsi": { + "area": 34424.49, + "population": 136890 + }, + "paracel-islands-yftc": { + "area": 0, + "population": 0 + }, + "paraguari-vemt": { + "area": 16455.23, + "population": 287287 + }, + "paraiba-aniv": { + "area": 57025.4, + "population": 1403133 + }, + "parana-arjl": { + "area": 198924.75, + "population": 4894623 + }, + "parma-uqrn": { + "area": 16181.76, + "population": 3040805 + }, + "pas-de-calais-egiw": { + "area": 12529.52, + "population": 1279808 + }, + "pasco-ydtb": { + "area": 23847.96, + "population": 589148 + }, + "pastaza-gdus": { + "area": 29593.87, + "population": 1976721 + }, + "pavlodar-abzq": { + "area": 123515.52, + "population": 842695 + }, + "paysandu-ikte": { + "area": 23954.92, + "population": 463733 + }, + "pechora-qwhs": { + "area": 37399.76, + "population": 315989 + }, + "peloponnisos-wqyw": { + "area": 21645.01, + "population": 1558959 + }, + "penghu-oghr": { + "area": 83.03, + "population": 53106 + }, + "pennsylvania-ylkc": { + "area": 119497.84, + "population": 4100801 + }, + "penza-mzew": { + "area": 43238.3, + "population": 365318 + }, + "perak-sxuz": { + "area": 21893.42, + "population": 2030270 + }, + "perm-cphx": { + "area": 160709.93, + "population": 1357831 + }, + "pernambuco-kfiq": { + "area": 98232.42, + "population": 2417048 + }, + "perugia-ozrc": { + "area": 20373.32, + "population": 3828466 + }, + "pest-akxn": { + "area": 19736.35, + "population": 2063418 + }, + "peten-kent": { + "area": 36411.48, + "population": 5477771 + }, + "phetchabun-mcvn": { + "area": 12675.99, + "population": 1672080 + }, + "phetchaburi-klyv": { + "area": 20142.87, + "population": 2657031 + }, + "phitsanulok-agvp": { + "area": 17477.01, + "population": 2305380 + }, + "phongsali-okhy": { + "area": 15364.96, + "population": 476675 + }, + "phra-nakhon-si-ayutthaya-eddh": { + "area": 25238.68, + "population": 3329217 + }, + "piaui-icjc": { + "area": 252965.62, + "population": 6224321 + }, + "pichincha-wrub": { + "area": 21363.62, + "population": 1426982 + }, + "pinar-del-rio-ejbc": { + "area": 15137.25, + "population": 1428603 + }, + "pirkanmaa-cenp": { + "area": 23761.28, + "population": 388296 + }, + "pita-rhov": { + "area": 12270.63, + "population": 623696 + }, + "piura-whve": { + "area": 42915.38, + "population": 1060196 + }, + "plateau-oako": { + "area": 27422.41, + "population": 5969236 + }, + "plateaux-kqwn": { + "area": 40115.87, + "population": 616569 + }, + "plateaux-myrm": { + "area": 24612.8, + "population": 3397375 + }, + "plovdiv-euus": { + "area": 13774.7, + "population": 842998 + }, + "plzensky-ysxk": { + "area": 21198.74, + "population": 2868776 + }, + "podlachian-amui": { + "area": 21096.75, + "population": 2532173 + }, + "podujevo-ozes": { + "area": 10941.52, + "population": 1794248 + }, + "poltava-usqb": { + "area": 28620.31, + "population": 2210429 + }, + "pomeranian-saur": { + "area": 20944.33, + "population": 2513879 + }, + "pomeroon-supenaam-iooq": { + "area": 48114.33, + "population": 176930 + }, + "poni-upoo": { + "area": 10676.25, + "population": 785927 + }, + "pool-umwl": { + "area": 35170.53, + "population": 540560 + }, + "portuguesa-epqi": { + "area": 17474.25, + "population": 535425 + }, + "potaro-siparuni-qaor": { + "area": 20783.62, + "population": 76427 + }, + "potenza-yerf": { + "area": 12293.5, + "population": 2310141 + }, + "potosi-iltd": { + "area": 117965.7, + "population": 1244162 + }, + "pouthisat-lcor": { + "area": 12956.23, + "population": 1148688 + }, + "powys-zcfq": { + "area": 20127.47, + "population": 5046146 + }, + "preah-vihear-xsol": { + "area": 13935.35, + "population": 1235496 + }, + "presidente-hayes-abmi": { + "area": 74180.74, + "population": 1295098 + }, + "presov-kpwz": { + "area": 15540.93, + "population": 1748102 + }, + "prey-veng-qaoq": { + "area": 20804.47, + "population": 1844506 + }, + "primor-ye-edix": { + "area": 167267.67, + "population": 1413237 + }, + "pskov-kakm": { + "area": 55706.15, + "population": 470659 + }, + "puebla-apfj": { + "area": 38561.86, + "population": 2461511 + }, + "puerto-rico-qqlt": { + "area": 9724.67, + "population": 3193694 + }, + "punjab-ghlp": { + "area": 204021.11, + "population": 50146377 + }, + "punjab-kjkb": { + "area": 51060.88, + "population": 21933232 + }, + "puno-vaqi": { + "area": 72268.53, + "population": 1785347 + }, + "puntarenas-evmt": { + "area": 12075.44, + "population": 1121969 + }, + "putumayo-ksux": { + "area": 26236.45, + "population": 1145910 + }, + "pwani-qxtg": { + "area": 34793.87, + "population": 2126844 + }, + "qaasuitsup-kommunia-kawz": { + "area": 594942.59, + "population": 14990 + }, + "qaraghandy-khxs": { + "area": 427736.23, + "population": 2918267 + }, + "qazvin-crno": { + "area": 20294.73, + "population": 1027192 + }, + "qeqqata-kommunia-mhwd": { + "area": 120859.44, + "population": 3045 + }, + "qinghai-cnbf": { + "area": 714516.53, + "population": 105996819 + }, + "qom-knnc": { + "area": 11583.42, + "population": 586280 + }, + "qostanay-qwtp": { + "area": 199910.8, + "population": 1363909 + }, + "quang-binh-wpei": { + "area": 17048.7, + "population": 4823983 + }, + "quang-nam-cneh": { + "area": 21859.88, + "population": 6185320 + }, + "quba-nxpp": { + "area": 36410.74, + "population": 4165995 + }, + "quebec-yegk": { + "area": 1514950.67, + "population": 5594985 + }, + "queensland-opqa": { + "area": 1742850.98, + "population": 5691528 + }, + "queretaro-mpre": { + "area": 12034.9, + "population": 768221 + }, + "quezon-scfg": { + "area": 22326.81, + "population": 7348705 + }, + "quintana-roo-ieho": { + "area": 45886.6, + "population": 2929070 + }, + "qyzylorda-emfx": { + "area": 224044.29, + "population": 1528561 + }, + "ragged-island-fprn": { + "area": 0, + "population": 0 + }, + "rajasthan-sfpr": { + "area": 343373.86, + "population": 147496454 + }, + "rajshahi-usrv": { + "area": 19338.62, + "population": 22689220 + }, + "rakhine-wotp": { + "area": 40988.23, + "population": 3234976 + }, + "rangpur-drru": { + "area": 15684.74, + "population": 18402273 + }, + "rapti-ewpq": { + "area": 10682.88, + "population": 2062494 + }, + "ratnapura-llsf": { + "area": 15535.82, + "population": 4848717 + }, + "razavi-khorasan-cdqd": { + "area": 136897.57, + "population": 6928896 + }, + "red-sea-zspo": { + "area": 201557.77, + "population": 4618354 + }, + "region-metropolitana-de-santiago-qwdi": { + "area": 16211.77, + "population": 363232 + }, + "relizane-ybmf": { + "area": 15231.67, + "population": 282239 + }, + "rennell-and-bellona-amuo": { + "area": 594.45, + "population": 12060 + }, + "rezeknes-mmcq": { + "area": 24344.79, + "population": 715052 + }, + "rheinland-pfalz-cfwr": { + "area": 22801.01, + "population": 5245329 + }, + "riau-zdhp": { + "area": 93494.46, + "population": 12754439 + }, + "rif-dimashq-smjb": { + "area": 17944.68, + "population": 1630636 + }, + "rift-valley-wlpc": { + "area": 182091.97, + "population": 16227856 + }, + "rio-claro-mayaro-ojxf": { + "area": 7860.1, + "population": 1358261 + }, + "rio-de-janeiro-mhoj": { + "area": 49650.13, + "population": 1221661 + }, + "rio-grande-do-norte-knon": { + "area": 54811.32, + "population": 1348654 + }, + "rio-grande-do-sul-auoi": { + "area": 283264.05, + "population": 6969826 + }, + "rio-negro-qxnf": { + "area": 203699.78, + "population": 3265574 + }, + "river-gee-uqio": { + "area": 10886.38, + "population": 522839 + }, + "river-nile-gpnj": { + "area": 126657.83, + "population": 2902149 + }, + "rivers-esqx": { + "area": 26082.14, + "population": 5677490 + }, + "rivne-ekqw": { + "area": 19883.87, + "population": 1535689 + }, + "rocha-ymzo": { + "area": 11356.74, + "population": 219850 + }, + "roi-et-gerl": { + "area": 20250.9, + "population": 2671282 + }, + "roma-sawi": { + "area": 24298.46, + "population": 4566060 + }, + "rondonia-gpoo": { + "area": 239869.21, + "population": 5902078 + }, + "roraima-itoh": { + "area": 226574.87, + "population": 5574966 + }, + "roscommon-ytzq": { + "area": 21349.18, + "population": 1493625 + }, + "rostov-ylrd": { + "area": 101646.71, + "population": 858808 + }, + "rotanokiri-exoo": { + "area": 12179.26, + "population": 1079803 + }, + "rotuma-kdui": { + "area": 4669.67, + "population": 185350 + }, + "rukwa-ygvp": { + "area": 27965.97, + "population": 1709475 + }, + "ruvuma-qivq": { + "area": 64763.03, + "population": 3958767 + }, + "ruyigi-kdjc": { + "area": 12725.5, + "population": 5347459 + }, + "ryanggang-ggfz": { + "area": 13590.74, + "population": 2793174 + }, + "ryazan-ggrj": { + "area": 39351.86, + "population": 332482 + }, + "sa-dah-kvqv": { + "area": 13442.29, + "population": 847832 + }, + "sa-kaeo-cufl": { + "area": 11712.89, + "population": 1545038 + }, + "saba-gckq": { + "area": 0, + "population": 0 + }, + "sabah-knxa": { + "area": 79358.65, + "population": 7359264 + }, + "sabha-sukt": { + "area": 17232.76, + "population": 71557 + }, + "sachsen-anhalt-arad": { + "area": 20574.49, + "population": 4733123 + }, + "sachsen-oiah": { + "area": 18306.32, + "population": 4211336 + }, + "sagaing-axrl": { + "area": 97503.03, + "population": 7695379 + }, + "sagarmatha-otrd": { + "area": 20443.13, + "population": 3946862 + }, + "saint-louis-iuhj": { + "area": 19254, + "population": 1544547 + }, + "sakha-yakutia-yxkf": { + "area": 3071260.13, + "population": 25948935 + }, + "sakhalin-wlzc": { + "area": 94267.55, + "population": 796462 + }, + "sakon-nakhon-qhrn": { + "area": 19111.58, + "population": 2520995 + }, + "sala-ad-din-simf": { + "area": 25532.7, + "population": 2285036 + }, + "salamanca-qrmw": { + "area": 12572.17, + "population": 1137308 + }, + "salamat-nrnf": { + "area": 70365.88, + "population": 881418 + }, + "salta-uunu": { + "area": 155914.29, + "population": 2499510 + }, + "salto-aobp": { + "area": 14444.17, + "population": 279618 + }, + "samangan-iybb": { + "area": 13574.45, + "population": 799093 + }, + "samar-vdxo": { + "area": 23370.5, + "population": 7692227 + }, + "samara-koze": { + "area": 53188.1, + "population": 449384 + }, + "samarkand-ansu": { + "area": 16735.07, + "population": 1254622 + }, + "samegrelo-zemo-svaneti-wjlp": { + "area": 22397.32, + "population": 1185442 + }, + "samsun-inzb": { + "area": 27127.23, + "population": 2803969 + }, + "san-andres-y-providencia-sbat": { + "area": 8.84, + "population": 386 + }, + "san-jose-qcby": { + "area": 10203.56, + "population": 197526 + }, + "san-juan-iczh": { + "area": 40670.1, + "population": 8709101 + }, + "san-juan-iind": { + "area": 88103.97, + "population": 1412422 + }, + "san-luis-amnp": { + "area": 77087.65, + "population": 1235816 + }, + "san-luis-potosi-wfta": { + "area": 64550.51, + "population": 4120440 + }, + "san-marcos-cvvw": { + "area": 11409.64, + "population": 1716474 + }, + "san-martin-orum": { + "area": 52590.25, + "population": 1299208 + }, + "san-pedro-qtmm": { + "area": 21070.02, + "population": 367855 + }, + "san-salvador-kzur": { + "area": 55.04, + "population": 1066 + }, + "sana-a-ylqp": { + "area": 12366.5, + "population": 779979 + }, + "sancti-spiritus-msbi": { + "area": 13768.85, + "population": 1299458 + }, + "sandaun-swut": { + "area": 37754.81, + "population": 675481 + }, + "sangha-mbaere-mqfz": { + "area": 18137.89, + "population": 138165 + }, + "sangha-oowm": { + "area": 58244.82, + "population": 895205 + }, + "sanguie-skxs": { + "area": 25292.9, + "population": 1861925 + }, + "sanliurfa-imci": { + "area": 19967.71, + "population": 2063935 + }, + "sanma-yzht": { + "area": 10132.15, + "population": 225962 + }, + "sanmatenga-alfl": { + "area": 20435.21, + "population": 1504328 + }, + "santa-barbara-qgiw": { + "area": 18516.72, + "population": 1562501 + }, + "santa-catarina-ourg": { + "area": 97601.57, + "population": 2401526 + }, + "santa-cruz-de-tenerife-ggog": { + "area": 3166.4, + "population": 286440 + }, + "santa-cruz-kptg": { + "area": 363369.12, + "population": 3832387 + }, + "santa-cruz-mjnn": { + "area": 246590.35, + "population": 3953166 + }, + "santa-fe-iyng": { + "area": 132938.72, + "population": 2131181 + }, + "santander-mmat": { + "area": 31207.83, + "population": 1363041 + }, + "santarem-sojz": { + "area": 13095.74, + "population": 1379425 + }, + "santiago-de-cuba-qfej": { + "area": 23795.72, + "population": 2245761 + }, + "santiago-del-estero-grns": { + "area": 135000, + "population": 2164226 + }, + "sao-paulo-eurp": { + "area": 254541.37, + "population": 6263093 + }, + "saratov-avzz": { + "area": 101953.41, + "population": 861400 + }, + "sarawak-cpxb": { + "area": 126805.19, + "population": 11759183 + }, + "sari-pul-eubi": { + "area": 16363.07, + "population": 963252 + }, + "sarthe-ebkd": { + "area": 18701.71, + "population": 1910256 + }, + "saskatchewan-cmli": { + "area": 647611.6, + "population": 2391746 + }, + "sassari-aeyz": { + "area": 25935.55, + "population": 4873696 + }, + "sava-atuj": { + "area": 24038.29, + "population": 1073067 + }, + "savanes-sjqm": { + "area": 40267.28, + "population": 3158498 + }, + "savannakhet-ulpr": { + "area": 31224.96, + "population": 968708 + }, + "savoie-zdke": { + "area": 11136.95, + "population": 1137565 + }, + "schleswig-holstein-gmaj": { + "area": 19730.12, + "population": 4538876 + }, + "sedhiou-qgha": { + "area": 14473.41, + "population": 1161050 + }, + "segou-aowd": { + "area": 62150.2, + "population": 970502 + }, + "seien-et-marne-bxka": { + "area": 12247.86, + "population": 1251037 + }, + "selangor-wkxy": { + "area": 17202.45, + "population": 1595256 + }, + "selenge-mhnj": { + "area": 45625.79, + "population": 94211 + }, + "semenawi-keyih-bahri-comw": { + "area": 22952.28, + "population": 1116480 + }, + "semnan-mukt": { + "area": 91653.06, + "population": 4638903 + }, + "sennar-dssz": { + "area": 36801.08, + "population": 843234 + }, + "sergipe-glra": { + "area": 23459.62, + "population": 577233 + }, + "seti-qarh": { + "area": 20353.19, + "population": 3929497 + }, + "setif-mbzf": { + "area": 13671.64, + "population": 253332 + }, + "severnaya-zemlya-awqp": { + "area": 1209.93, + "population": 10223 + }, + "sevilla-oxma": { + "area": 14049.07, + "population": 1270911 + }, + "sfax-ktva": { + "area": 14068.68, + "population": 1036709 + }, + "shaanxi-kyfb": { + "area": 205183.39, + "population": 30438465 + }, + "shabeellaha-dhexe-ueua": { + "area": 24200.79, + "population": 505948 + }, + "shabeellaha-hoose-sxum": { + "area": 25728.86, + "population": 537894 + }, + "shabwah-mpqr": { + "area": 40593.7, + "population": 2560323 + }, + "shamal-sina-uhqn": { + "area": 27518.52, + "population": 2723666 + }, + "shan-qqtq": { + "area": 155082.49, + "population": 12239808 + }, + "shandong-ajfr": { + "area": 160652.46, + "population": 23832408 + }, + "shanxi-ubft": { + "area": 155925.31, + "population": 23131147 + }, + "shetland-islands-ypae": { + "area": 1163.09, + "population": 291598 + }, + "shinyanga-kdvn": { + "area": 20228.14, + "population": 1236485 + }, + "shizuoka-ycjg": { + "area": 17539.37, + "population": 5201763 + }, + "shkoder-gmnr": { + "area": 12689.94, + "population": 1215025 + }, + "shropshire-mhvl": { + "area": 19521.48, + "population": 4894218 + }, + "shumen-ahrd": { + "area": 23181.36, + "population": 1418676 + }, + "sichuan-eufy": { + "area": 485745.35, + "population": 72059160 + }, + "sidi-bel-abbes-kfbb": { + "area": 39486.91, + "population": 731683 + }, + "sidi-bou-zid-ifqk": { + "area": 22439.79, + "population": 1653569 + }, + "siemreab-vyop": { + "area": 24181.55, + "population": 2143915 + }, + "siguiri-juvq": { + "area": 17805.8, + "population": 905039 + }, + "siirt-yozc": { + "area": 27073.7, + "population": 2798435 + }, + "sikasso-fzwk": { + "area": 71670.01, + "population": 1119157 + }, + "sila-rfnx": { + "area": 37610.87, + "population": 471122 + }, + "silesian-igbk": { + "area": 21801.02, + "population": 2616704 + }, + "simiyu-iyyt": { + "area": 24668.29, + "population": 1507898 + }, + "sinaloa-ahmy": { + "area": 64082.25, + "population": 4090549 + }, + "sind-atud": { + "area": 143087.08, + "population": 35169393 + }, + "singida-gfvo": { + "area": 48899.23, + "population": 2989061 + }, + "sipaliwini-lvya": { + "area": 111773.9, + "population": 444473 + }, + "sissili-amog": { + "area": 12103.07, + "population": 890962 + }, + "sistan-and-baluchestan-avku": { + "area": 176794.8, + "population": 8948243 + }, + "sivas-ofzw": { + "area": 28004.07, + "population": 2894602 + }, + "skane-ouua": { + "area": 12304.57, + "population": 280499 + }, + "skikda-odba": { + "area": 15030.68, + "population": 278515 + }, + "smolensk-qrwu": { + "area": 50278.43, + "population": 424800 + }, + "sodermanland-sfuh": { + "area": 23899.33, + "population": 544816 + }, + "sofala-ohfw": { + "area": 67363.86, + "population": 2564718 + }, + "sofia-corh": { + "area": 20639.64, + "population": 1263125 + }, + "sofia-kmae": { + "area": 52013.87, + "population": 2321894 + }, + "sogn-og-fjordane-iuak": { + "area": 19638.11, + "population": 242996 + }, + "sokoto-mjsi": { + "area": 31348.13, + "population": 6823776 + }, + "somali-eiea": { + "area": 312672.64, + "population": 30886017 + }, + "somaliland-gapg": { + "area": 170987.02, + "population": 5096159 + }, + "somerset-drhz": { + "area": 20859.98, + "population": 5229794 + }, + "somme-sgzc": { + "area": 12263.16, + "population": 1252601 + }, + "somogy-ymes": { + "area": 14184.23, + "population": 1482948 + }, + "son-la-amzp": { + "area": 14787.72, + "population": 4184232 + }, + "songkhla-eojn": { + "area": 12348.53, + "population": 1628885 + }, + "sonora-otbs": { + "area": 181130.34, + "population": 11562056 + }, + "sor-trondelag-sxam": { + "area": 34935.41, + "population": 432279 + }, + "soria-comu": { + "area": 10535.84, + "population": 953097 + }, + "soriano-ixbv": { + "area": 20436, + "population": 395611 + }, + "souk-ahras-uqmy": { + "area": 12274.01, + "population": 227434 + }, + "soum-whaq": { + "area": 12734.15, + "population": 937418 + }, + "souss-massa-draa-mbhy": { + "area": 72552, + "population": 4361025 + }, + "south-australia-cutu": { + "area": 995998.16, + "population": 3252574 + }, + "south-carolina-izro": { + "area": 81492.87, + "population": 2796586 + }, + "south-dakota-oerr": { + "area": 198570.25, + "population": 6814324 + }, + "south-gyeongsang-edoo": { + "area": 13185.62, + "population": 6483726 + }, + "south-jeolla-wooe": { + "area": 20223.87, + "population": 9944621 + }, + "south-kazakhstan-qtgc": { + "area": 114588.78, + "population": 781792 + }, + "south-khorasan-usnp": { + "area": 84458.17, + "population": 4274742 + }, + "south-kordufan-ygzz": { + "area": 139320.51, + "population": 3192293 + }, + "southern-ctpk": { + "area": 27562.89, + "population": 109261 + }, + "southern-darfur-iopm": { + "area": 79237.84, + "population": 1815600 + }, + "southern-highlands-wdcq": { + "area": 25079.9, + "population": 448711 + }, + "southern-nations-nationalities-and-peoples-ohez": { + "area": 114873.18, + "population": 11347251 + }, + "southern-ncas": { + "area": 23979.65, + "population": 2459175 + }, + "southern-orts": { + "area": 85432.84, + "population": 2017825 + }, + "southern-ostrobothnia-ihkk": { + "area": 30950.58, + "population": 505781 + }, + "southern-savonia-ewkd": { + "area": 26400.88, + "population": 431432 + }, + "southland-ukrs": { + "area": 32499.7, + "population": 554318 + }, + "sremski-xjrs": { + "area": 12625.54, + "population": 1126918 + }, + "st-eustatius-wdkr": { + "area": 9.7, + "population": 4077 + }, + "stara-zagora-hmrg": { + "area": 13262.05, + "population": 811624 + }, + "stavropol-mizd": { + "area": 66731.15, + "population": 563808 + }, + "steiermark-afps": { + "area": 16711.85, + "population": 1748515 + }, + "sterea-ellada-inyu": { + "area": 36662.53, + "population": 2640580 + }, + "stoeng-treng-jiov": { + "area": 11383.46, + "population": 1009247 + }, + "stredocesky-ihft": { + "area": 24580.11, + "population": 3326369 + }, + "subcarpathian-ajux": { + "area": 18135.73, + "population": 2176772 + }, + "sucre-glea": { + "area": 11463.69, + "population": 500691 + }, + "sucre-qsqk": { + "area": 13123.29, + "population": 402108 + }, + "sucumbios-ejuw": { + "area": 18219.85, + "population": 1216994 + }, + "sud-bandama-sksp": { + "area": 19178.96, + "population": 1504366 + }, + "sud-kivu-katu": { + "area": 63919.42, + "population": 2373271 + }, + "sud-mukj": { + "area": 47221.93, + "population": 2581459 + }, + "sud-ouest-yxkl": { + "area": 24563.17, + "population": 1342783 + }, + "sudurland-ysfc": { + "area": 26161.29, + "population": 83372 + }, + "suffolk-peqe": { + "area": 15892.57, + "population": 3984416 + }, + "suhbaatar-quqi": { + "area": 81703.61, + "population": 168707 + }, + "sulawesi-barat-zpku": { + "area": 21482.38, + "population": 2930609 + }, + "sulawesi-selatan-thkw": { + "area": 48312.71, + "population": 6590780 + }, + "sulawesi-tengah-lskw": { + "area": 64085.49, + "population": 8742490 + }, + "sulawesi-tenggara-ctkn": { + "area": 39186.33, + "population": 5345768 + }, + "sulawesi-utara-ojkh": { + "area": 15255.56, + "population": 2081151 + }, + "sultan-kudarat-pnsz": { + "area": 35.48, + "population": 11677 + }, + "sumatera-barat-vokk": { + "area": 44008.34, + "population": 6003583 + }, + "sumatera-selatan-enkt": { + "area": 85903.96, + "population": 11718948 + }, + "sumatera-utara-qqkv": { + "area": 73830.23, + "population": 10071861 + }, + "sumy-kcqr": { + "area": 23253.64, + "population": 1795946 + }, + "surat-thani-wngk": { + "area": 29103.26, + "population": 3838990 + }, + "surkhandarya-usso": { + "area": 19499.57, + "population": 1461875 + }, + "surt-otkn": { + "area": 46587.41, + "population": 193448 + }, + "svalbard-eeck": { + "area": 77047.95, + "population": 953366 + }, + "sverdlovsk-eefj": { + "area": 193529.89, + "population": 1635125 + }, + "swietokrzyskie-yobp": { + "area": 11497.38, + "population": 1379993 + }, + "syddanmark-ykbt": { + "area": 25739.24, + "population": 2989091 + }, + "sylhet-iprt": { + "area": 12086.13, + "population": 14180173 + }, + "ta-izz-ctzr": { + "area": 11471.39, + "population": 723523 + }, + "tabasco-hhvb": { + "area": 24968.51, + "population": 1593810 + }, + "tabora-wnvt": { + "area": 76906.18, + "population": 4701041 + }, + "tabuk-spql": { + "area": 100914.71, + "population": 1786925 + }, + "tachira-kuql": { + "area": 10644.27, + "population": 326149 + }, + "tacna-uiaf": { + "area": 15843.25, + "population": 391397 + }, + "tacuarembo-yekg": { + "area": 25453.98, + "population": 492752 + }, + "tadjourah-jsgf": { + "area": 11345.53, + "population": 490316 + }, + "tadla-azilal-idhz": { + "area": 16655.12, + "population": 1001122 + }, + "tadzhikistan-territories-uxcw": { + "area": 28804.16, + "population": 1889537 + }, + "tafea-wskq": { + "area": 1557.46, + "population": 34734 + }, + "tagant-tjfm": { + "area": 95318.1, + "population": 413250 + }, + "tahoua-gpff": { + "area": 105836.95, + "population": 2078868 + }, + "tak-ojvr": { + "area": 25500.65, + "population": 3363772 + }, + "takhar-tbah": { + "area": 20485.16, + "population": 1205909 + }, + "talas-orlv": { + "area": 11896.34, + "population": 382103 + }, + "tamanghasset-mozx": { + "area": 610764.87, + "population": 11317332 + }, + "tamaulipas-apvq": { + "area": 79962.09, + "population": 5104204 + }, + "tambacounda-shqy": { + "area": 40122.19, + "population": 3218584 + }, + "tambov-evce": { + "area": 34339.74, + "population": 290135 + }, + "tamil-nadu-sqvd": { + "area": 133797.84, + "population": 57472947 + }, + "tandjile-trlq": { + "area": 17896.74, + "population": 224178 + }, + "tanga-oovu": { + "area": 28243.6, + "population": 1726445 + }, + "tanger-tetouan-eqeu": { + "area": 20972.9, + "population": 1260659 + }, + "tanintharyi-ydrp": { + "area": 42391.01, + "population": 3345689 + }, + "tapoa-gpat": { + "area": 14534.13, + "population": 1069923 + }, + "taraba-cqbj": { + "area": 61243.14, + "population": 13331241 + }, + "tarapaca-mrpq": { + "area": 42047.9, + "population": 942103 + }, + "tarija-grth": { + "area": 36712.91, + "population": 387204 + }, + "tartu-aymq": { + "area": 10528.87, + "population": 296771 + }, + "tashauz-mmct": { + "area": 72996.07, + "population": 910433 + }, + "tashkent-awvo": { + "area": 15199.72, + "population": 1139518 + }, + "tasmania-kptu": { + "area": 72103.71, + "population": 235465 + }, + "tataouine-qrma": { + "area": 48021.62, + "population": 3538672 + }, + "tatarstan-aotw": { + "area": 68062.65, + "population": 575058 + }, + "tawi-tawi-khxg": { + "area": 612.7, + "population": 201667 + }, + "taza-al-hoceima-taounate-otew": { + "area": 26085.76, + "population": 1567988 + }, + "tebessa-iqzy": { + "area": 13321.2, + "population": 246839 + }, + "tehran-ytij": { + "area": 14558.47, + "population": 736858 + }, + "telangana-mfvw": { + "area": 115038.79, + "population": 49414982 + }, + "telemark-ceyx": { + "area": 15186.96, + "population": 187919 + }, + "teleorman-gcnr": { + "area": 11220.95, + "population": 915143 + }, + "telimele-jadt": { + "area": 17863.24, + "population": 907959 + }, + "temotu-wpdk": { + "area": 720.59, + "population": 14619 + }, + "tennessee-geup": { + "area": 111570.14, + "population": 3828746 + }, + "terengganu-smxz": { + "area": 13321, + "population": 1235313 + }, + "ternopil-whse": { + "area": 14104.14, + "population": 1089304 + }, + "teruel-ofof": { + "area": 21331.36, + "population": 1929685 + }, + "tete-kjfx": { + "area": 101729.68, + "population": 3873115 + }, + "texas-mguh": { + "area": 691559.79, + "population": 23732217 + }, + "thaba-tseka-wjua": { + "area": 15591.8, + "population": 1092151 + }, + "thanh-hoa-okef": { + "area": 11701.18, + "population": 3310885 + }, + "the-snares-mvou": { + "area": 21.99, + "population": 375 + }, + "thessalia-yhyh": { + "area": 15696.87, + "population": 1130550 + }, + "thies-mpqd": { + "area": 16715.78, + "population": 1340932 + }, + "thimphu-agoq": { + "area": 10668.77, + "population": 204038 + }, + "three-kings-islands-qafu": { + "area": 697.46, + "population": 11896 + }, + "thuringen-mxiz": { + "area": 16056.41, + "population": 3693746 + }, + "tianjin-krnw": { + "area": 12615.24, + "population": 1871440 + }, + "tiaret-stza": { + "area": 20212.46, + "population": 374532 + }, + "tibesti-teli": { + "area": 120337.42, + "population": 1507371 + }, + "tierra-del-fuego-sqnb": { + "area": 25170.36, + "population": 403514 + }, + "tigray-ucnt": { + "area": 49935.02, + "population": 4932615 + }, + "tillaberi-yrfg": { + "area": 92979.88, + "population": 1826327 + }, + "timbuktu-mrwf": { + "area": 506320.59, + "population": 7906411 + }, + "timis-seme": { + "area": 16535.43, + "population": 1348574 + }, + "tindouf-ewdf": { + "area": 152953.06, + "population": 2834185 + }, + "tiris-zemmour-xshf": { + "area": 259891.76, + "population": 1126756 + }, + "tirol-sgpt": { + "area": 15747.99, + "population": 1647669 + }, + "tocantins-kczu": { + "area": 280267.76, + "population": 6896101 + }, + "tochigi-kfff": { + "area": 12951.35, + "population": 3841065 + }, + "tokelau-cqiy": { + "area": 0, + "population": 0 + }, + "toledo-yvoz": { + "area": 15436.53, + "population": 1396425 + }, + "tolima-gone": { + "area": 26206.6, + "population": 1144606 + }, + "tomsk-gews": { + "area": 312995.16, + "population": 2644482 + }, + "torba-ckkb": { + "area": 1757.11, + "population": 39186 + }, + "tov-udnh": { + "area": 77624.59, + "population": 160284 + }, + "transcarpathia-qncy": { + "area": 13216.74, + "population": 1020767 + }, + "trarza-mefd": { + "area": 73787.08, + "population": 319902 + }, + "tripura-ylvj": { + "area": 10978.87, + "population": 4715982 + }, + "troms-ccap": { + "area": 28276.98, + "population": 349890 + }, + "tucuman-ennz": { + "area": 23298.38, + "population": 373503 + }, + "tukums-qtmk": { + "area": 12138.53, + "population": 356531 + }, + "tula-cvuo": { + "area": 25440.83, + "population": 214948 + }, + "tungurahua-ylux": { + "area": 13300.15, + "population": 888383 + }, + "turin-yfaa": { + "area": 17089.18, + "population": 3211323 + }, + "tuva-ecty": { + "area": 169544.28, + "population": 1432472 + }, + "tuy-updk": { + "area": 16530.61, + "population": 1216893 + }, + "tuyen-quang-kpzr": { + "area": 28693.95, + "population": 8119043 + }, + "tver-qdaj": { + "area": 84493.93, + "population": 713885 + }, + "tyumen-uppf": { + "area": 159127.19, + "population": 1344458 + }, + "ubon-ratchathani-mzil": { + "area": 23570.49, + "population": 3109166 + }, + "ucayali-hmry": { + "area": 104392.71, + "population": 2578954 + }, + "udmurt-cave": { + "area": 41914.32, + "population": 354132 + }, + "udon-thani-iuqh": { + "area": 18633.36, + "population": 2457913 + }, + "uige-alyd": { + "area": 62648.75, + "population": 1570990 + }, + "ul-yanovsk-uovf": { + "area": 37063.48, + "population": 313148 + }, + "unity-smuv": { + "area": 35557.08, + "population": 622788 + }, + "upper-demerara-berbice-ynhw": { + "area": 55485.87, + "population": 204037 + }, + "upper-nile-kxdz": { + "area": 76179.96, + "population": 1334303 + }, + "upper-takutu-upper-essequibo-cohg": { + "area": 26481.01, + "population": 97378 + }, + "upper-west-avqy": { + "area": 27785.9, + "population": 3444925 + }, + "uppsala-aeug": { + "area": 13104.52, + "population": 298734 + }, + "uruzgan-ebhn": { + "area": 11685.33, + "population": 687886 + }, + "uruzgan-unxj": { + "area": 17558.92, + "population": 1033649 + }, + "usulutan-xbjc": { + "area": 21233.16, + "population": 6453553 + }, + "utah-whas": { + "area": 219566.5, + "population": 7534851 + }, + "uthai-thani-ymvt": { + "area": 14371.3, + "population": 1895708 + }, + "uttar-pradesh-aaxe": { + "area": 240526.75, + "population": 103318415 + }, + "uttaradit-swtq": { + "area": 14370.44, + "population": 1895594 + }, + "uttarakhand-ygxi": { + "area": 52636.21, + "population": 22609917 + }, + "uusimaa-gpng": { + "area": 25323.27, + "population": 413822 + }, + "uvs-ccng": { + "area": 68820.34, + "population": 142104 + }, + "vakaga-ahth": { + "area": 46234.9, + "population": 352195 + }, + "vakinankaratra-dtjh": { + "area": 24253.58, + "population": 1082677 + }, + "valencia-ydbr": { + "area": 11734.69, + "population": 1061547 + }, + "valle-del-cauca-urfx": { + "area": 21402.94, + "population": 934801 + }, + "vallee-du-bandama-oyqf": { + "area": 28607.4, + "population": 2243916 + }, + "valparaiso-mfuk": { + "area": 16874.22, + "population": 378075 + }, + "van-ujcg": { + "area": 27081.4, + "population": 2799231 + }, + "varmland-aeuq": { + "area": 22246.37, + "population": 507134 + }, + "vasterbotten-ykth": { + "area": 58516.64, + "population": 1333962 + }, + "vasternorrland-upug": { + "area": 23073.54, + "population": 525991 + }, + "vastra-gotaland-yxuc": { + "area": 29888.41, + "population": 681344 + }, + "vatovavy-fitovinany-abdm": { + "area": 21567.41, + "population": 962767 + }, + "vaupes-ihfh": { + "area": 52339.9, + "population": 2286011 + }, + "veliko-tarnovo-zews": { + "area": 15404.97, + "population": 942768 + }, + "vendee-lozh": { + "area": 12865.8, + "population": 1314157 + }, + "venezuela-oxzs": { + "area": 0, + "population": 0 + }, + "ventspils-cuhk": { + "area": 13060.15, + "population": 383601 + }, + "veracruz-kuoh": { + "area": 71789.25, + "population": 4582509 + }, + "veraguas-gsiv": { + "area": 22359.12, + "population": 1209870 + }, + "vermont-ujdo": { + "area": 24647.42, + "population": 845824 + }, + "vesturland-ajfn": { + "area": 24603.57, + "population": 78408 + }, + "veszprem-czmj": { + "area": 16276.16, + "population": 1701659 + }, + "vichada-oozm": { + "area": 99000.3, + "population": 4323962 + }, + "victoria-svnw": { + "area": 231703.41, + "population": 756661 + }, + "vienne-cpzi": { + "area": 13142.72, + "population": 1342442 + }, + "vientiane-qhaq": { + "area": 24072.1, + "population": 746802 + }, + "vila-real-ytjc": { + "area": 17485.09, + "population": 1841772 + }, + "villa-clara-cpbg": { + "area": 13191.54, + "population": 1244974 + }, + "vinnytsya-yoxi": { + "area": 26508.21, + "population": 2047306 + }, + "viqueque-wkyr": { + "area": 15550.03, + "population": 1242909 + }, + "virginia-oedb": { + "area": 105776.54, + "population": 3629928 + }, + "viseu-uvjd": { + "area": 22068.59, + "population": 2324569 + }, + "vitebsk-anmn": { + "area": 39723.49, + "population": 1816745 + }, + "vladimir-mqvt": { + "area": 29335.84, + "population": 247857 + }, + "volgograd-omkg": { + "area": 112520.39, + "population": 950680 + }, + "vologda-ynvr": { + "area": 145260.15, + "population": 1227296 + }, + "volta-ihni": { + "area": 20920.89, + "population": 2593794 + }, + "volyn-irxk": { + "area": 19827.11, + "population": 1531305 + }, + "voronezh-svdp": { + "area": 52192.58, + "population": 440973 + }, + "vosges-oszk": { + "area": 15438.42, + "population": 1576932 + }, + "vratsa-lhwu": { + "area": 11580.38, + "population": 708708 + }, + "vysocina-kamr": { + "area": 18528.07, + "population": 2507360 + }, + "wadi-al-hayaa-qrtp": { + "area": 32471.99, + "population": 134836 + }, + "wadi-fira-wtem": { + "area": 55313.75, + "population": 692871 + }, + "waikato-gxqe": { + "area": 26841.2, + "population": 457806 + }, + "wangdi-phodrang-ipqp": { + "area": 29231.86, + "population": 559054 + }, + "wardak-wpjg": { + "area": 10273.53, + "population": 604777 + }, + "warmian-masurian-epsm": { + "area": 24463.92, + "population": 2936323 + }, + "warrap-kuzy": { + "area": 34615.23, + "population": 606291 + }, + "washington-exgl": { + "area": 176251.87, + "population": 6048425 + }, + "wasit-vawa": { + "area": 26268.71, + "population": 2350904 + }, + "west-azarbaijan-eyim": { + "area": 37517.21, + "population": 1898886 + }, + "west-bank-cjlo": { + "area": 5810.67, + "population": 4371545 + }, + "west-bengal-isbk": { + "area": 88218.35, + "population": 37894249 + }, + "west-bosnia-ayyu": { + "area": 17930.76, + "population": 1146496 + }, + "west-coast-ucji": { + "area": 23223.71, + "population": 396106 + }, + "west-kazakhstan-aqxa": { + "area": 153602.69, + "population": 1047968 + }, + "west-new-britain-qila": { + "area": 23745.87, + "population": 424843 + }, + "west-pomeranian-mafy": { + "area": 23937.07, + "population": 2873087 + }, + "west-virginia-ivga": { + "area": 63139.31, + "population": 2166748 + }, + "western-adsp": { + "area": 102119.54, + "population": 1827047 + }, + "western-afoi": { + "area": 25625.32, + "population": 519875 + }, + "western-alun": { + "area": 17751.6, + "population": 704603 + }, + "western-australia-sxjw": { + "area": 2545787.66, + "population": 8313633 + }, + "western-bahr-el-ghazal-uwvs": { + "area": 87661.5, + "population": 1535404 + }, + "western-cape-qngk": { + "area": 134198.54, + "population": 6387032 + }, + "western-cutu": { + "area": 129538.06, + "population": 3059540 + }, + "western-darfur-kvpq": { + "area": 46697.53, + "population": 1069995 + }, + "western-equatoria-yffx": { + "area": 82248.17, + "population": 1440589 + }, + "western-mygs": { + "area": 11833.7, + "population": 5842107 + }, + "western-qwvr": { + "area": 25790.61, + "population": 3197547 + }, + "western-sahara-gozf": { + "area": 90237.49, + "population": 603253 + }, + "western-tobago-lmvd": { + "area": 212.45, + "population": 36712 + }, + "wexford-iasb": { + "area": 15377.91, + "population": 1075864 + }, + "white-nile-uwno": { + "area": 38891.6, + "population": 891135 + }, + "wisconsin-ekgt": { + "area": 169620.42, + "population": 5820854 + }, + "worodougou-wiso": { + "area": 22702.04, + "population": 1780710 + }, + "wouleu-ntem-qnwu": { + "area": 38519.82, + "population": 308529 + }, + "wyoming-ajgj": { + "area": 253015.62, + "population": 8682723 + }, + "xaignabouri-hdan": { + "area": 17019.96, + "population": 528019 + }, + "xiangkhoang-hqaf": { + "area": 12229.18, + "population": 379392 + }, + "xinjiang-oasq": { + "area": 1628458.94, + "population": 241577987 + }, + "xizang-kzsk": { + "area": 1132195.53, + "population": 167958498 + }, + "yala-ywjs": { + "area": 11436.87, + "population": 1508629 + }, + "yamagata-gofu": { + "area": 16840.59, + "population": 4994523 + }, + "yamal-gosw": { + "area": 50598.25, + "population": 427502 + }, + "yamal-nenets-aqsx": { + "area": 710952.36, + "population": 6006804 + }, + "yamanashi-gwhj": { + "area": 12463.85, + "population": 3696485 + }, + "yaracuy-yxqn": { + "area": 12298.91, + "population": 376848 + }, + "yaroslavl-cksd": { + "area": 36218.65, + "population": 306010 + }, + "yatenga-ajue": { + "area": 10945.46, + "population": 805745 + }, + "yazd-slnb": { + "area": 127872.5, + "population": 6472103 + }, + "yevrey-itcn": { + "area": 36066.04, + "population": 304720 + }, + "yobe-sitq": { + "area": 45555.84, + "population": 9916472 + }, + "yomou-dhxs": { + "area": 19908, + "population": 1011891 + }, + "yonne-juzl": { + "area": 13764.94, + "population": 1405998 + }, + "yoro-chgi": { + "area": 12638.72, + "population": 1066495 + }, + "yozgat-oecd": { + "area": 13820.38, + "population": 1428525 + }, + "ysyk-kol-hqmi": { + "area": 42625.37, + "population": 1369100 + }, + "yucatan-mpkz": { + "area": 38929.49, + "population": 2484978 + }, + "yukon-hsje": { + "area": 482510.21, + "population": 1781997 + }, + "yunnan-mbuv": { + "area": 386829.85, + "population": 57385282 + }, + "zabul-ftpc": { + "area": 17250.13, + "population": 1015471 + }, + "zacatecas-pbpt": { + "area": 80890.56, + "population": 5163471 + }, + "zadarska-mglb": { + "area": 15025.39, + "population": 1070871 + }, + "zagrebacka-cijs": { + "area": 15967.05, + "population": 1137984 + }, + "zaire-ikgg": { + "area": 44864.48, + "population": 1125029 + }, + "zajecarski-lotj": { + "area": 19883.05, + "population": 1774701 + }, + "zambales-gjfs": { + "area": 12949.47, + "population": 4262222 + }, + "zambezia-eefu": { + "area": 103548.88, + "population": 3942377 + }, + "zamboanga-del-norte-qvgj": { + "area": 20084.65, + "population": 6610713 + }, + "zamfara-iavl": { + "area": 34969.58, + "population": 7612083 + }, + "zamora-chinchipe-qcur": { + "area": 10588.09, + "population": 707231 + }, + "zamora-qhdj": { + "area": 10900.59, + "population": 986093 + }, + "zanjan-qsks": { + "area": 22473.26, + "population": 1137455 + }, + "zanzan-qsqb": { + "area": 38299.37, + "population": 3004139 + }, + "zaporizhzhya-cjcw": { + "area": 27681.54, + "population": 2137925 + }, + "zaragoza-ajbv": { + "area": 17423.3, + "population": 1576152 + }, + "zenica-doboj-kqyp": { + "area": 20092.69, + "population": 1284730 + }, + "zhambyl-qhpv": { + "area": 141117.97, + "population": 962790 + }, + "zhejiang-amww": { + "area": 115069.06, + "population": 17070220 + }, + "zhytomyr-iczk": { + "area": 29434.68, + "population": 2273325 + }, + "zinder-qsfh": { + "area": 146787.26, + "population": 2883222 + }, + "zlatiborski-gqtk": { + "area": 13007.51, + "population": 1161011 + }, + "zou-qmpu": { + "area": 18709.02, + "population": 1868862 + }, + "zulia-szqo": { + "area": 48067.28, + "population": 1472818 + }, + "zurich-kdpk": { + "area": 23710.9, + "population": 4952329 + } +} diff --git a/scripts/build-region-geo.mjs b/scripts/build-region-geo.mjs new file mode 100644 index 0000000..41e1bd8 --- /dev/null +++ b/scripts/build-region-geo.mjs @@ -0,0 +1,165 @@ +/** + * scripts/build-region-geo.mjs — build the region **geo** manifest: + * `public/region-geo.json` = `{ "": { "area": , "population": }, ... }`. + * + * For every sub-region (region key, see `public/region-keys.json`) this records + * two facts the v0.28 per-region stats model (and its area×population weighting) + * needs: + * - `area` — the region's **total** surface area in km², from + * `@turf/area` on the FINAL generated feature + * (`public/regions.geo.json`) — turf reports square metres, + * so the raw value is divided by 1e6 — rounded to 2 decimals. + * - `population` — the region's share of its **parent country's** real + * population (Natural Earth 1:10m admin-0 `POP_EST`, from the + * committed `public/ne-admin0-pop.json`), distributed across + * the country's sub-regions **proportional to each region's + * area** (a first-order "uniform population density within a + * country" approximation — Natural Earth admin-1, which the + * region generator uses, carries no per-unit population). + * + * Population distribution uses **largest remainder** per country, so the sum of + * a country's sub-region populations is EXACTLY the country's POP_EST, and the + * sum over all 1,800 regions is EXACTLY the sum of the 177 repo countries' + * POP_EST (7,654,092,021) — no rounding drift. + * + * Method — plain turf on the pre-generated geo file (turf is allowed in .mjs + * scripts; it is deliberately kept OUT of the TS typecheck program, see the + * v0.17 note). The population source is the COMMITTED `public/ne-admin0-pop.json` + * (a small derived table produced once by `scripts/extract-admin0-pop.mjs` from + * the 13 MB Natural Earth admin-0 file, which we do NOT commit) — so this build + * is fully reproducible in CI. + * + * Output contract (asserted by scripts/region-geo.test.ts): + * - one entry for **every** region key, keyed identically to + * `public/region-keys.json` (same key set, 1,800 entries); + * - `area` is a finite number >= 0 (km², <= 2 decimals); + * - `population` is a non-negative integer; + * - the total population across all entries is a non-negative integer. + * + * Deterministic + idempotent: fixed inputs (the geo file, the key manifest, and + * the committed population table), pure turf, largest-remainder with a + * (remainder desc, key asc) tie-break, and stable sorted key order → a re-run + * rewrites a byte-identical file (the test re-runs this script and compares + * bytes). + * + * Usage: `node scripts/build-region-geo.mjs` (or `npm run build:region-geo`). + */ +import { readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import area from '@turf/area'; +import { feature } from '@turf/helpers'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const root = path.resolve(here, '..'); + +const round2 = (v) => Math.round(v * 100) / 100; + +// --- load the inputs (plain parse) ------------------------------------------ +const geoPath = path.join(root, 'public', 'regions.geo.json'); +const keysPath = path.join(root, 'public', 'region-keys.json'); +const popPath = path.join(root, 'public', 'ne-admin0-pop.json'); + +let geo, idToKey, popByCountry; +try { + geo = JSON.parse(readFileSync(geoPath, 'utf8')); + idToKey = JSON.parse(readFileSync(keysPath, 'utf8')); + popByCountry = JSON.parse(readFileSync(popPath, 'utf8')); +} catch (err) { + console.error( + `[build-region-geo] could not load the inputs (${geoPath}, ${keysPath}, ${popPath}). ` + + `Is public/ne-admin0-pop.json present? Regenerate it with scripts/extract-admin0-pop.mjs.`, + ); + throw err; +} + +/** + * One pass over the geo features: for each region key record its (unrounded) + * turf area and its parent ISO_A3, then group the keys by parent so the + * population can be distributed per country. + */ +const regionArea = new Map(); // key -> unrounded area (km²) +const regionParent = new Map(); // key -> parent ISO_A3 +for (const f of geo.features ?? []) { + const id = f.id == null ? '' : String(f.id); + const key = idToKey[id]; + if (!key) continue; // a geo feature with no manifest entry is skipped + // @turf/area reports square METRES; the manifest stores km². + const a = area(feature(f.geometry)) / 1e6; + regionArea.set(key, a); + regionParent.set(key, typeof f.properties?.parent === 'string' ? f.properties.parent : ''); +} + +// Group region keys by parent country (file order is stable, but we sort for +// deterministic population distribution). +const keysByCountry = new Map(); // parent -> string[] of keys +for (const [key, parent] of regionParent) { + if (!parent) continue; // a region with no parent has no population source + const list = keysByCountry.get(parent); + if (list) list.push(key); + else keysByCountry.set(parent, [key]); +} + +const population = new Map(); // key -> integer population +for (const [parent, keys] of keysByCountry) { + keys.sort(); // deterministic ordering for the tie-break + const P = Number.isFinite(popByCountry[parent]) ? Math.max(0, popByCountry[parent]) : 0; + const totalArea = keys.reduce((s, k) => s + (regionArea.get(k) || 0), 0); + if (P <= 0 || totalArea <= 0) { + for (const k of keys) population.set(k, 0); + continue; + } + // Largest remainder: floor each share, then hand out the leftover 1s to the + // regions with the largest fractional remainders (ties broken by key asc). + const shares = keys.map((k) => { + const a = regionArea.get(k) || 0; + const ideal = (P * a) / totalArea; + return { k, floor: Math.floor(ideal), rem: ideal - Math.floor(ideal) }; + }); + const distributed = shares.reduce((s, x) => s + x.floor, 0); + let deficit = P - distributed; // 0 <= deficit < keys.length + const order = shares + .map((x, i) => ({ i, rem: x.rem, k: x.k })) + .sort((a, b) => (b.rem - a.rem) || (a.k < b.k ? -1 : a.k > b.k ? 1 : 0)); + for (let i = 0; i < order.length && deficit > 0; i++) { + shares[order[i].i].floor += 1; + deficit -= 1; + } + for (const x of shares) population.set(x.k, x.floor); +} + +// --- assemble the manifest (sorted keys) ------------------------------------- +const expectedKeys = Object.values(idToKey); +const manifest = {}; +for (const k of [...expectedKeys].sort()) { + const a = regionArea.get(k); + const p = population.get(k); + if (a === undefined || p === undefined) { + throw new Error(`region key ${JSON.stringify(k)} has no computed area/population`); + } + manifest[k] = { area: round2(a), population: p }; +} + +// --- hard assertions (the test re-checks these against the committed file) ---- +if (Object.keys(manifest).length !== expectedKeys.length) { + throw new Error( + `expected ${expectedKeys.length} region-geo entries (one per region key), got ${Object.keys(manifest).length}`, + ); +} +let totalPop = 0; +let zeros = 0; +for (const [k, v] of Object.entries(manifest)) { + if (!Number.isFinite(v.area) || v.area < 0) throw new Error(`bad area for ${k}: ${JSON.stringify(v)}`); + if (!Number.isInteger(v.population) || v.population < 0) throw new Error(`bad population for ${k}: ${JSON.stringify(v)}`); + if (v.area === 0) zeros += 1; + totalPop += v.population; +} + +// --- write idempotently (sorted keys, 2-space indent, trailing newline) ------ +writeFileSync(path.join(root, 'public', 'region-geo.json'), JSON.stringify(manifest, null, 2) + '\n'); + +// --- summary ------------------------------------------------------------------ +console.log( + `[build-region-geo] wrote public/region-geo.json — ${Object.keys(manifest).length} region keys` + + `${zeros ? ` (${zeros} zero-area regions)` : ''}, total population ${totalPop.toLocaleString('en-US')}`, +); diff --git a/scripts/extract-admin0-pop.mjs b/scripts/extract-admin0-pop.mjs new file mode 100644 index 0000000..3a5a41c --- /dev/null +++ b/scripts/extract-admin0-pop.mjs @@ -0,0 +1,78 @@ +/** + * scripts/extract-admin0-pop.mjs — one-time extraction of the population + * SOURCE for the region-geo manifest: `public/ne-admin0-pop.json`. + * + * Output = `{ "": , ... }` — the estimated population + * (POP_EST) of every Natural Earth 1:10m **admin-0 country** code, keeping + * the **maximum** POP_EST seen for each code (Natural Earth ships a few + * duplicated / broken-out features per code, e.g. the `TLC` stubs, whose + * POP_EST is 0; the real country always wins by having the larger value). + * Only codes with a finite, positive POP_EST are recorded (a 0/null POP_EST + * has nothing to distribute). Keys are sorted; the file is written with + * 2-space indent + trailing newline (deterministic, committed). + * + * Why a separate committed file: `public/region-geo.json` (built by + * `build-region-geo.mjs`) needs a per-country population, but the raw + * Natural Earth GeoJSON (13 MB) is a *source* file we deliberately do NOT + * commit (see the AGENTS.md data-pipeline note). This script distils it + * into a tiny (a few KB) derived table that IS committed, so the region-geo + * builder stays fully reproducible in CI without the source present. + * Re-run only when the underlying Natural Earth data changes. + * + * The 1:10m admin-1 file (the one the region generator uses) carries NO + * population; the 1:10m admin-0 file does (uppercase `POP_EST` / `ADM0_A3`). + * + * Usage: + * NE_ADMIN0=/path/to/ne_10m_admin_0_countries.geojson node scripts/extract-admin0-pop.mjs + * (default source: /tmp/ne10m_admin0.geojson) + */ +import { readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const root = path.resolve(here, '..'); + +const src = process.env.NE_ADMIN0 || '/tmp/ne10m_admin0.geojson'; +let raw; +try { + raw = readFileSync(src, 'utf8'); +} catch (err) { + console.error( + `[extract-admin0-pop] could not read the Natural Earth admin-0 source at ${src}. ` + + `Download it (ne_10m_admin_0_countries.geojson) and re-run with NE_ADMIN0 pointing at it.`, + ); + throw err; +} + +const geo = JSON.parse(raw); + +/** ADM0_A3 -> max finite, positive POP_EST. */ +const byCode = new Map(); +for (const f of geo.features ?? []) { + const p = f.properties ?? {}; + const code = typeof p.ADM0_A3 === 'string' ? p.ADM0_A3 : ''; + const pop = typeof p.POP_EST === 'number' ? p.POP_EST : NaN; + if (!code || !Number.isFinite(pop) || pop <= 0) continue; + const prev = byCode.get(code); + if (prev === undefined || pop > prev) byCode.set(code, Math.round(pop)); +} + +if (byCode.size === 0) { + throw new Error('[extract-admin0-pop] no finite positive POP_EST found in the source — wrong file?'); +} + +// Deterministic: sorted keys, 2-space indent, trailing newline. +const keys = [...byCode.keys()].sort(); +const manifest = {}; +for (const k of keys) manifest[k] = byCode.get(k); + +const out = path.join(root, 'public', 'ne-admin0-pop.json'); +writeFileSync(out, JSON.stringify(manifest, null, 2) + '\n'); + +let sum = 0; +for (const v of byCode.values()) sum += v; +console.log( + `[extract-admin0-pop] wrote public/ne-admin0-pop.json — ${byCode.size} country codes, ` + + `total population ${sum.toLocaleString('en-US')} (Natural Earth 1:10m admin-0, max POP_EST per code)`, +); diff --git a/scripts/region-geo.test.ts b/scripts/region-geo.test.ts new file mode 100644 index 0000000..81153a7 --- /dev/null +++ b/scripts/region-geo.test.ts @@ -0,0 +1,139 @@ +/** + * scripts/region-geo.test.ts — validation for the committed + * `public/region-geo.json` (built by `scripts/build-region-geo.mjs`). + * + * The manifest maps each region key to its { area (km²), population } — the + * weighting source for the per-region stats model (v0.28, #46): a nation's + * four 0–100 stats are derived as the area×population-weighted median of the + * stats of the regions it owns. It is a load-bearing data artifact — this + * test pins its contract. + * + * The `area` is the turf area of the region's final geometry (km², 2 dp); the + * `population` is the country's Natural-Earth admin-0 `POP_EST` (from the + * committed `public/ne-admin0-pop.json`) distributed across its sub-regions + * proportional to area via largest-remainder, so every country's sub-regions + * sum EXACTLY to the country's total (and the global total is the sum over the + * 177 repo countries). This is a dependency-free test: it reads the JSON with + * plain `fs`/`JSON.parse` (no `@turf`/geojson types, which would pull + * `@types/geojson` into the tsc program and break `WorldMap.tsx` — see the + * v0.17 note). The determinism test re-runs the (turf `.mjs`) builder and + * compares bytes, matching that the builder is idempotent. + */ +import { readFileSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const root = path.resolve(here, '..'); + +function readJson(rel: string): Record { + return JSON.parse(readFileSync(path.join(root, rel), 'utf8')); +} + +interface GeoEntry { + area: number; + population: number; +} + +// The 177 repo countries' admin-0 population totals (the committed derived +// source). Only the countries actually present in the region dataset appear. +const admin0 = readJson('public/ne-admin0-pop.json') as Record; +// feature id -> region key +const manifest = readJson('public/region-keys.json') as Record; +// feature id -> { name, parent (ISO_A3) } +const regGeo = readJson('public/regions.geo.json') as { + features?: Array<{ id?: string | number; properties?: { name?: string; parent?: string } }>; +}; + +// region key -> parent ISO_A3 (via the key manifest + the geo feature parent). +const keyToParent: Record = {}; +for (const f of regGeo.features ?? []) { + const key = manifest[String(f.id)]; + const parent = f.properties?.parent; + if (key && parent) keyToParent[key] = parent; +} + +const geo = readJson('public/region-geo.json') as Record; +const keys = Object.values(manifest); + +describe('region-geo.json', () => { + it('has exactly one entry per region key (same key set as region-keys.json)', () => { + expect(new Set(Object.keys(geo))).toEqual(new Set(keys)); + expect(Object.keys(geo)).toHaveLength(1800); + }); + + it('is keyed in sorted (deterministic) order', () => { + const ks = Object.keys(geo); + expect(ks).toEqual([...ks].sort()); + }); + + it('every entry is a finite { area ≥ 0 with ≤ 2 dp, integer population ≥ 0 }', () => { + for (const [k, v] of Object.entries(geo)) { + expect(Number.isFinite(v.area), `non-finite area for ${k}`).toBe(true); + expect(Number.isFinite(v.population), `non-finite population for ${k}`).toBe(true); + expect(v.area >= 0, `negative area for ${k}: ${v.area}`).toBe(true); + // area is recorded to at most 2 decimal places (km²) + expect(Math.round(v.area * 100) / 100 === v.area, `area over 2dp for ${k}: ${v.area}`).toBe(true); + expect(Number.isInteger(v.population), `non-integer population for ${k}: ${v.population}`).toBe(true); + expect(v.population >= 0, `negative population for ${k}: ${v.population}`).toBe(true); + } + }); + + it('the global population total equals the sum of the 177 repo countries\' admin-0 totals', () => { + // The distribution preserved each country's total exactly, so the grand + // total over all sub-regions is the sum over the repo countries only. + const total = Object.values(geo).reduce((s, v) => s + v.population, 0); + expect(total).toBe(7654092021); + }); + + it('preserves each country\'s population total across its sub-regions (largest-remainder)', () => { + // For every parent country present in the region dataset, the sum of its + // sub-regions' populations must equal that country's admin-0 POP_EST + // EXACTLY — this is the invariant the largest-remainder distribution + // guarantees (no population is created or lost by the split). + const byParent: Record = {}; + for (const [k, v] of Object.entries(geo)) { + const p = keyToParent[k]; + if (!p) throw new Error(`no parent for region key ${k}`); + byParent[p] = (byParent[p] ?? 0) + v.population; + } + expect(Object.keys(byParent).length).toBe(177); // the 177 repo countries + for (const [p, sum] of Object.entries(byParent)) { + expect(admin0[p], `missing admin-0 population for ${p}`).toBeDefined(); + expect(sum, `population drift for ${p}: sub-regions ${sum} vs admin-0 ${admin0[p]}`).toBe(admin0[p]); + } + }); + + it('spot-checks well-known regions for exact area + population', () => { + const is = (k: string, area: number, population: number) => { + const v = geo[k]; + expect(v, `missing geo for ${k}`).toBeDefined(); + expect(v!.area, `area for ${k}`).toBe(area); + expect(v!.population, `population for ${k}`).toBe(population); + }; + is('california-kikh', 413329.21, 14184195); // US-CA + is('arkhangel-sk-lhbk', 305257.7, 2579108); // RU-ARK mainland core + is('pechora-qwhs', 37399.76, 315989); // RU-PEO (Arctic-coast band, #41) + is('yamal-gosw', 50598.25, 427502); // RU-YAM (Arctic-coast band, #41) + is('new-earth-eikt', 15721.27, 132828); // RU-ARK-NE (island group, #31) + is('severnaya-zemlya-awqp', 1209.93, 10223); // RU-ARK-SZ (island group, #31) + is('ain-lqtt', 22557.77, 2304126); // FR-01 (Ain) + }); + + it('is deterministic: re-running the builder rewrites a byte-identical file', () => { + const file = path.join(root, 'public', 'region-geo.json'); + const before = readFileSync(file, 'utf8'); + // The builder is cwd-independent (it resolves the repo root from its own + // location) and idempotent: same input files (regions.geo.json + + // region-keys.json + ne-admin0-pop.json) + pure turf + fixed 2-dp rounding + // + stable (sorted) key order → identical bytes. + execFileSync(process.execPath, [path.join(root, 'scripts', 'build-region-geo.mjs')], { + cwd: root, + stdio: 'pipe', + }); + const after = readFileSync(file, 'utf8'); + expect(after).toBe(before); + }); +}); diff --git a/server/db.test.ts b/server/db.test.ts index 6bca890..fcf4ecf 100644 --- a/server/db.test.ts +++ b/server/db.test.ts @@ -26,6 +26,11 @@ function makeState(id: string): GameState { unowned: [], pendingNationOps: [], markers: [], + // (v0.28, #46) a v0.28+ save carries per-region stats; the fake 'GBR' + // region here holds a copy of the uk's (single-region) stats. + regionStats: { + GBR: { military: 80, economy: 70, stability: 60, population: 50 }, + }, }; } @@ -109,6 +114,39 @@ describe('openDb', () => { db.close(); }); + it('(v0.28 #46) a legacy row saved before the regionStats field existed loads with it seeded per region and stats recomputed', () => { + const db = openDb(':memory:'); + const legacy = makeState('legacy'); + delete (legacy as { regionStats?: Record }).regionStats; // a pre-v0.28 save (field absent) + db.save(legacy); + const loaded = db.load('legacy'); + expect(loaded).not.toBeNull(); + // each owned region is seeded with a copy of its owner's stats… + expect(loaded!.regionStats).toEqual({ + GBR: { military: 80, economy: 70, stability: 60, population: 50 }, + }); + // …and a single-region nation's derived stats equal the seeded ones + // (the weighted median of identical values is that value). + expect(loaded!.nations.uk.stats).toEqual({ military: 80, economy: 70, stability: 60, population: 50 }); + db.close(); + }); + + it('(v0.28 #46) a v0.28+ row (regionStats present) loads UNCHANGED — no recompute on load', () => { + const db = openDb(':memory:'); + const s = makeState('current'); + // Diverge one region's stats from the nation level to prove load does not + // recompute (a v0.28+ save keeps its stored derived stats exactly). + s.regionStats = { GBR: { military: 99, economy: 70, stability: 60, population: 50 } }; + s.nations = { uk: { ...s.nations.uk, stats: { military: 42, economy: 70, stability: 60, population: 50 } } }; + db.save(s); + const loaded = db.load('current'); + expect(loaded).not.toBeNull(); + expect(loaded!.regionStats).toEqual({ GBR: { military: 99, economy: 70, stability: 60, population: 50 } }); + // the nation's stats are NOT recomputed from the region on load (they stay 42) + expect(loaded!.nations.uk.stats.military).toBe(42); + db.close(); + }); + it('delete removes a game by id and reports absence otherwise', () => { const db = openDb(':memory:'); db.save(makeState('a')); diff --git a/server/db.ts b/server/db.ts index 395521c..27d716c 100644 --- a/server/db.ts +++ b/server/db.ts @@ -2,6 +2,7 @@ import { mkdirSync } from 'node:fs'; import path from 'node:path'; import Database from 'better-sqlite3'; +import { migrateRegionStats } from '../shared/protocol'; import type { GameState } from '../shared/types'; export interface Db { @@ -39,7 +40,7 @@ export function openDb(filePath: string): Db { const row = byId.get(id) as { data: string } | undefined; if (!row) return null; try { - const state = JSON.parse(row.data) as GameState; + let state = JSON.parse(row.data) as GameState; // (v0.22, #16 P3) Rows saved before `GameState.unowned` existed load // with it defaulted: a legacy game has no no-man's-land regions. if (state.unowned === undefined) state.unowned = []; @@ -48,6 +49,13 @@ export function openDb(filePath: string): Db { if (state.pendingNationOps === undefined) state.pendingNationOps = []; // (v0.27, #44) Same for map markers: a legacy game has none placed. if (state.markers === undefined) state.markers = []; + // (v0.28, #46) Rows saved before per-region stats existed are migrated + // ONCE at load time (and never re-migrated afterwards — the field is + // then present): each owned region is seeded with a copy of its owner's + // nation-level stats and the nations' stats are recomputed as the + // (uniform-weight) area×population-weighted median, so no data is lost + // and a single-region nation keeps exactly its old stats. + state = migrateRegionStats(state); return state; } catch { return null; diff --git a/server/engine.test.ts b/server/engine.test.ts index acca724..2398a8f 100644 --- a/server/engine.test.ts +++ b/server/engine.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { GameState, MarkerType, Preset, RoundItem } from '../shared/types'; -import { makeEvent, markerId, MAX_MARKERS, type MarkerSpec, type NationOp, type TurnResult } from '../shared/protocol'; +import type { GameState, MarkerType, Nation, NationStats, Preset, RegionStats, RoundItem } from '../shared/types'; +import { makeEvent, markerId, MAX_MARKERS, seedRegionStats, type MarkerSpec, type NationOp, type TurnResult } from '../shared/protocol'; import type { LlmClient, ToolDefinition, WireMessage } from './llm'; import { makeStubLlmClient } from './stub'; import { @@ -71,6 +71,11 @@ const preset: Preset = { }; function baseState(): GameState { + const nations: GameState['nations'] = { + uk: { ...preset.nations[0], stats: { ...preset.nations[0].stats }, territory: [...preset.nations[0].territory], relations: { ...preset.nations[0].relations } }, + us: { ...preset.nations[1], stats: { ...preset.nations[1].stats }, territory: [...preset.nations[1].territory], relations: { ...preset.nations[1].relations } }, + de: { ...preset.nations[2], stats: { ...preset.nations[2].stats }, territory: [...preset.nations[2].territory], relations: { ...preset.nations[2].relations } }, + }; return { id: 'g1', presetId: 'test', @@ -78,17 +83,16 @@ function baseState(): GameState { playerNationId: 'uk', turn: 1, date: '1939-09-01', - nations: { - uk: { ...preset.nations[0], stats: { ...preset.nations[0].stats }, territory: [...preset.nations[0].territory], relations: { ...preset.nations[0].relations } }, - us: { ...preset.nations[1], stats: { ...preset.nations[1].stats }, territory: [...preset.nations[1].territory], relations: { ...preset.nations[1].relations } }, - de: { ...preset.nations[2], stats: { ...preset.nations[2].stats }, territory: [...preset.nations[2].territory], relations: { ...preset.nations[2].relations } }, - }, + nations, events: [], pending: [], version: 1, unowned: [], pendingNationOps: [], markers: [], + // (v0.28, #46) every owned region starts as a copy of its owner's stats, + // so a fresh state's derived stats equal its nation-level stats. + regionStats: seedRegionStats(nations), }; } @@ -1908,3 +1912,211 @@ describe('markers (point placement, v0.27 #44)', () => { expect(state.markers[0].ownerNationId).toBe('uk'); // uk gained the region this turn }); }); + +// --- Per-region stats (v0.28, #46): the derived-stats model ------------------ +// +// A nation's 0–100 stats are DERIVED as the area×population-weighted median of +// the stats of the regions it owns (`GameState.regionStats`). These tests pin +// the turn-level maintenance of that model in `applyTurnResult`: +// - a transferred region keeps its OWN stats (the conquest is what makes a +// nation's regions diverge); +// - a released region's entry is deleted (no man's land carries none); +// - a re-claimed region with no entry is re-inited to the gainer's stats; +// - a 0-region nation keeps its (directly-maintained) stats; +// - the end-of-turn recompute weights by area×population when `geoOf` is +// provided (and degrades to a uniform median without it). +// +// `applyTurnResult` is used directly (not `runTurn`) so the tests are pure and +// don't depend on the LLM: the result objects below are constructed directly. + +const NS = (military: number, economy: number, stability: number, population: number): NationStats => ({ + military, + economy, + stability, + population, +}); + +const RS = (military: number, economy: number, stability: number, population: number): RegionStats => ({ + military, + economy, + stability, + population, +}); + +function mkNation(id: string, stats: NationStats, territory: string[]): Nation { + return { id, name: id.toUpperCase(), type: 'major', stats, territory, relations: {} }; +} + +function mkTurnState( + nations: Record, + regionStats: Record, + unowned: string[] = [], +): GameState { + return { + id: 'g', + presetId: 'p', + presetTitle: 'P', + playerNationId: Object.keys(nations)[0] ?? '', + turn: 1, + date: '1939-09-01', + nations, + events: [], + pending: [], + version: 1, + unowned, + pendingNationOps: [], + markers: [], + regionStats, + }; +} + +function mkTurnResult(partial: Partial): TurnResult { + return { + date: '1939-09-03', + events: partial.events ?? [], + stateChanges: partial.stateChanges ?? [], + relations: partial.relations ?? [], + territoryChanges: partial.territoryChanges ?? [], + unowned: partial.unowned ?? [], + nationOps: partial.nationOps ?? [], + markers: partial.markers ?? { add: [], remove: [] }, + }; +} + +describe('applyTurnResult — per-region stats (v0.28, #46)', () => { + it('a transferred region keeps its OWN stats; the conqueror\'s aggregate blends them', () => { + // uk owns g1 (military 80), de owns d1 (military 95). de conquers g1. + const state = mkTurnState( + { + uk: mkNation('uk', NS(80, 70, 60, 50), ['g1']), + de: mkNation('de', NS(95, 90, 75, 90), ['d1']), + }, + { + g1: RS(80, 70, 60, 50), + d1: RS(95, 90, 75, 90), + }, + ); + const next = applyTurnResult( + state, + mkTurnResult({ territoryChanges: [{ nation: 'de', gain: ['g1'], lose: [] }] }), + ); + // territory moved + expect(next.nations.uk.territory).toEqual([]); + expect(next.nations.de.territory).toEqual(['d1', 'g1']); + // the transferred region KEPT its own (uk's) stats — not re-inited to de's + expect(next.regionStats.g1).toEqual(RS(80, 70, 60, 50)); + expect(next.regionStats.d1).toEqual(RS(95, 90, 75, 90)); + // de's derived stats are the (uniform) median of its two regions → pulled + // toward the conquered region's lower values + expect(next.nations.de.stats).toEqual(RS(80, 70, 60, 50)); + // uk (now 0 regions) keeps its directly-maintained stats untouched + expect(next.nations.uk.stats).toEqual(NS(80, 70, 60, 50)); + }); + + it('a released region leaves no-man\'s land and its stats entry is deleted', () => { + const state = mkTurnState( + { uk: mkNation('uk', NS(80, 70, 60, 50), ['g1', 'g2']) }, + { g1: RS(80, 70, 60, 50), g2: RS(60, 60, 60, 60) }, + ); + const next = applyTurnResult(state, mkTurnResult({ unowned: ['g1'] })); + expect(next.unowned).toEqual(['g1']); + expect(next.nations.uk.territory).toEqual(['g2']); + // g1's stats left the table with the region; g2's stay + expect(next.regionStats).toEqual({ g2: RS(60, 60, 60, 60) }); + // uk's derived stats now come only from g2 + expect(next.nations.uk.stats).toEqual(RS(60, 60, 60, 60)); + }); + + it('a re-claimed region with no entry is re-inited to the gainer\'s current stats', () => { + // g1 is unowned (released earlier; its stats entry was deleted). de owns d1. + const deStats = NS(95, 90, 75, 90); + const state = mkTurnState( + { + uk: mkNation('uk', NS(80, 70, 60, 50), []), + de: mkNation('de', deStats, ['d1']), + }, + { d1: RS(95, 90, 75, 90) }, // g1 has NO entry (it was released) + ['g1'], + ); + const next = applyTurnResult(state, mkTurnResult({ territoryChanges: [{ nation: 'de', gain: ['g1'], lose: [] }] })); + expect(next.unowned).toEqual([]); // g1 leaves no man's land + expect(next.nations.de.territory).toEqual(['d1', 'g1']); + // g1 was re-inited as a copy of de's (pre-stateChanges) stats + expect(next.regionStats.g1).toEqual(RS(95, 90, 75, 90)); + // both of de's regions now hold identical stats → derived stats = those stats + expect(next.nations.de.stats).toEqual(RS(95, 90, 75, 90)); + }); + + it('a 0-region nation keeps its directly-maintained stats (the recompute skips it)', () => { + const state = mkTurnState( + { uk: mkNation('uk', NS(80, 70, 60, 50), []) }, + {}, + ); + const next = applyTurnResult( + state, + mkTurnResult({ stateChanges: [{ nation: 'uk', stat: 'military', op: 'set', value: 99 }] }), + ); + expect(next.nations.uk.territory).toEqual([]); + // no regions to aggregate → the nation-level update sticks + expect(next.nations.uk.stats.military).toBe(99); + expect(next.regionStats).toEqual({}); + }); + + it('the recompute weights by area×population: the larger region dominates (geoOf)', () => { + // uk owns two regions: a small one (military 50) and a large one (military + // 90). With area×population weighting the large region's value wins; without + // geo the uniform median picks the lower value (reached first at half-weight). + const state = mkTurnState( + { uk: mkNation('uk', NS(50, 50, 50, 50), ['big', 'small']) }, + { big: RS(90, 90, 90, 90), small: RS(50, 50, 50, 50) }, + ); + const geoOf = (k: string) => + k === 'big' ? { area: 1000, population: 1000 } : { area: 10, population: 100 }; + + const weighted = applyTurnResult(state, mkTurnResult({}), undefined, undefined, geoOf); + expect(weighted.nations.uk.stats).toEqual(RS(90, 90, 90, 90)); // the large region dominates + + const uniform = applyTurnResult(state, mkTurnResult({})); // no geoOf + expect(uniform.nations.uk.stats).toEqual(RS(50, 50, 50, 50)); // uniform → lower value first + }); + + it('a nation-level stat change re-applies to every owned region (before the recompute)', () => { + // uk owns two identical regions. A nation-level `add` applies to each region + // and the derived (uniform) median tracks it; a 0-region sibling keeps its + // own nation-level update. + const state = mkTurnState( + { + uk: mkNation('uk', NS(50, 50, 50, 50), ['a', 'b']), + de: mkNation('de', NS(50, 50, 50, 50), []), + }, + { a: RS(50, 50, 50, 50), b: RS(50, 50, 50, 50) }, + ); + const next = applyTurnResult( + state, + mkTurnResult({ + stateChanges: [ + { nation: 'uk', stat: 'economy', op: 'add', value: 20 }, + { nation: 'de', stat: 'military', op: 'set', value: 77 }, + ], + }), + ); + // uk: both regions +20 economy → derived economy 70 + expect(next.regionStats.a).toEqual(RS(50, 70, 50, 50)); + expect(next.regionStats.b).toEqual(RS(50, 70, 50, 50)); + expect(next.nations.uk.stats).toEqual(RS(50, 70, 50, 50)); + // de: 0 regions → its nation-level set sticks + expect(next.nations.de.stats.military).toBe(77); + }); + + it('the original state is never mutated', () => { + const state = mkTurnState( + { uk: mkNation('uk', NS(80, 70, 60, 50), ['g1']), de: mkNation('de', NS(95, 90, 75, 90), ['d1']) }, + { g1: RS(80, 70, 60, 50), d1: RS(95, 90, 75, 90) }, + ); + const beforeNations = JSON.stringify(state.nations); + const beforeRegions = JSON.stringify(state.regionStats); + applyTurnResult(state, mkTurnResult({ territoryChanges: [{ nation: 'de', gain: ['g1'], lose: [] }] })); + expect(JSON.stringify(state.nations)).toBe(beforeNations); + expect(JSON.stringify(state.regionStats)).toBe(beforeRegions); + }); +}); diff --git a/server/engine.ts b/server/engine.ts index af386bb..13e4226 100644 --- a/server/engine.ts +++ b/server/engine.ts @@ -19,6 +19,7 @@ import { EVENT_TYPES, RELATION_STATUSES } from '../shared/types'; import { addDays, clampStat, + deriveNationStats, fnv1a32, isDateLike, isMarkerType, @@ -592,15 +593,26 @@ function genericUnclearEvent(state: GameState): TurnEventSpec { * deterministic offset; a spec whose key has no known centroid is dropped. * Without it no marker can be placed (all adds are dropped), which keeps the * engine pure and lets tests inject their own data. + * + * `geoOf` (v0.28, #46) resolves a region key to its area + population + * (from public/region-geo.json via server/regions.ts), used to weight the + * end-of-turn recompute of each nation's derived stats as the + * area×population-weighted median of its regions' stats. Without it the + * recompute degrades to an unweighted median (every region weight 1) — still + * region-consistent, just unweighted. The engine stays pure either way. */ export function applyTurnResult( state: GameState, result: TurnResult, validKeys?: ReadonlySet, centroidOf?: (key: string) => { lat: number; lon: number; spread: number } | undefined, + geoOf?: (key: string) => { area: number; population: number } | undefined, ): GameState { const next: GameState = JSON.parse(JSON.stringify(state)); next.version = (state.version ?? 0) + 1; // every mutation is a new revision + // (v0.28, #46) per-region stats table; a legacy/corrupt clone without the + // field starts empty (the turn then re-inits claimed regions as it goes). + next.regionStats = next.regionStats ?? {}; const actingTurn = state.turn; // this action resolves the current turn next.turn = state.turn + 1; // then we advance to the next turn to play next.date = isDateLike(result.date) ? result.date : addDays(state.date, 2); @@ -609,11 +621,6 @@ export function applyTurnResult( for (const ev of result.events) { next.events.push(makeEvent(ev, actingTurn, next.date)); } - for (const c of result.stateChanges) { - const n = next.nations[c.nation]; - if (!n) continue; - n.stats[c.stat] = c.op === 'set' ? clampStat(c.value) : clampStat(n.stats[c.stat] + c.value); - } for (const r of result.relations) { const a = next.nations[r.a]; const b = next.nations[r.b]; @@ -663,6 +670,10 @@ export function applyTurnResult( if (owners.length !== 1) continue; // 0 owners (never assigned) or 2+ (corrupt): no-op unowned.add(key); next.nations[owners[0]].territory = next.nations[owners[0]].territory.filter((k) => k !== key); + // (v0.28, #46) a released region leaves no-man's land ownerless — its + // per-region stats leave the table with it (no man's land carries none; + // a later re-claim re-inits the entry from the gainer, see below). + delete next.regionStats[key]; } // (2) Claims against the post-release ownership snapshot. @@ -709,10 +720,53 @@ export function applyTurnResult( } next.nations[gainer].territory.push(key); unowned.delete(key); // re-claimed: the key leaves no man's land + // (v0.28, #46) a transferred region keeps its own per-region stats (the + // conquest is what makes a nation's regions diverge); only a re-claimed + // region that has no entry left (it was released earlier this turn) is + // re-inited as a copy of the gainer's current stats, so the recompute + // below never sees a hole in the table. + if (!next.regionStats[key]) next.regionStats[key] = { ...next.nations[gainer].stats }; } next.unowned = [...unowned]; + // --- Per-region stats (v0.28, #46): nation-level stateChanges re-applied to + // the regions, then every nation's stats recomputed as the derived + // area×population-weighted median of its regions --- + // + // (a) A nation-level stat change (`stateChanges`) is re-interpreted as + // applying to EVERY region the nation owns: each owned region's copy of + // that stat gets the same `set`/`add` (clamped to 0–100), and the + // nation-level stat is updated too (it is kept as-is for nations the + // recompute below does not touch — see (b) — and overwritten for the + // rest, where the regions are the source of truth). + // + // (b) RECOMPUTE: every nation's `stats` is re-derived as the weighted + // median of its owned regions' stats (via deriveNationStats, weighted + // by area×population when `geoOf` is available, unweighted otherwise). + // Two guards keep degenerate states sane: a nation with NO regions + // keeps its stats untouched (it has no regions to aggregate), and a + // nation none of whose regions has a stats entry keeps its stats + // too (deriveNationStats would return zeros, which would be wrong). + // This recompute is what makes a conquered region's foreign stats + // blend into the conqueror's aggregate and keeps the derived stats in + // lockstep with the per-region table across the turn's changes. + for (const c of result.stateChanges) { + const n = next.nations[c.nation]; + if (!n) continue; + for (const key of n.territory) { + const rs = next.regionStats[key]; + if (!rs) continue; // a key with no entry is skipped (recompute does the same) + rs[c.stat] = c.op === 'set' ? clampStat(c.value) : clampStat(rs[c.stat] + c.value); + } + n.stats[c.stat] = c.op === 'set' ? clampStat(c.value) : clampStat(n.stats[c.stat] + c.value); + } + for (const n of Object.values(next.nations)) { + if (n.territory.length === 0) continue; // 0-region guard: no regions to aggregate + if (!n.territory.some((k) => next.regionStats[k])) continue; // no entry: keep n.stats + n.stats = deriveNationStats(n.territory, next.regionStats, geoOf); + } + // --- Map markers (v0.27, #44) --- // // Markers are anchored to a region key and are processed against the FINAL @@ -1115,8 +1169,16 @@ export async function runTurn(opts: { * the existing markers' owners still get recomputed. */ centroidOf?: (key: string) => { lat: number; lon: number; spread: number } | undefined; + /** + * (v0.28, #46) Region-key -> area/population resolver (from + * public/region-geo.json). Present, the end-of-turn recompute of each + * nation's derived stats weights each owned region by area×population; + * absent, the recompute degrades to an unweighted (uniform) median — + * still region-consistent, just unweighted. + */ + geoOf?: (key: string) => { area: number; population: number } | undefined; }): Promise<{ state: GameState; result: TurnResult }> { - const { llm, preset, state, items, regionReference, validRegionKeys, getNeighbours, centroidOf } = opts; + const { llm, preset, state, items, regionReference, validRegionKeys, getNeighbours, centroidOf, geoOf } = opts; const chatWithTools = llm.chatWithTools; const useTools = getNeighbours !== undefined && chatWithTools !== undefined; const system = buildSystemPrompt(preset, useTools); @@ -1181,7 +1243,7 @@ export async function runTurn(opts: { }; } - return { state: applyTurnResult(state, result, validRegionKeys, centroidOf), result }; + return { state: applyTurnResult(state, result, validRegionKeys, centroidOf, geoOf), result }; } // --- Advisor (brainstorm) ----------------------------------------------------- diff --git a/server/index.ts b/server/index.ts index af8a062..a309f04 100644 --- a/server/index.ts +++ b/server/index.ts @@ -8,7 +8,7 @@ import { existsSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { deepClone } from '../shared/protocol'; +import { deepClone, seedRegionStats } from '../shared/protocol'; import type { AddActionRequest, AddDiplomacyRequest, @@ -22,7 +22,7 @@ import { createLlmClient } from './llm'; import { makeStubLlmClient } from './stub'; import { askAdvisor, confirmNationOp, queueRoundItem, rejectNationOp, removeRoundItem, runTurn } from './engine'; import { buildRegionMap } from './map'; -import { allTerritoryKeys, centroidOf, expandCountriesToKeys, hasAdjacency, hasCentroids, loadRegions, neighboursOf, referenceForKeys, validKeys } from './regions'; +import { allTerritoryKeys, centroidOf, expandCountriesToKeys, geoOf, hasAdjacency, hasCentroids, hasGeo, loadRegions, neighboursOf, referenceForKeys, validKeys } from './regions'; import { createPresetStore } from './presets'; import { openDb, type Db } from './db'; @@ -136,11 +136,18 @@ app.post('/api/games', (req: Request, res) => { pendingNationOps: [], // (v0.27, #44) No markers on a fresh game; the LLM places them over time. markers: [], + // (v0.28, #46) Per-region stats are seeded below, once territories hold + // real region keys — each region starts as a copy of its owner's initial + // stats, so the derived aggregate equals the nation's stats on turn 1. + regionStats: {}, }; // v0.19 (#16 P1): expand each nation's ISO_A3 country list to the region keys // of all its sub-regions, so ownership (and the choropleth) is at region // granularity from the very first turn. Preset JSONs stay country-level. for (const n of Object.values(state.nations)) n.territory = expandCountriesToKeys(n.territory); + // (v0.28, #46): seed per-region stats AFTER the expansion so each owned + // region key gets its owner's initial stats (see seedRegionStats). + state.regionStats = seedRegionStats(state.nations); db.save(state); res.json({ game: state }); }); @@ -295,6 +302,11 @@ app.post('/api/games/:id/jump', async (req, res) => { // centroid (without it, marker adds are dropped; existing markers are // still maintained). centroidOf: hasCentroids() ? (key) => centroidOf(key) : undefined, + // (v0.28, #46) when the region-geo manifest (area + population) loaded, + // hand the engine the resolver so the end-of-turn recompute weights each + // owned region by area×population (without it the aggregate degrades to + // an unweighted median — still region-consistent, just unweighted). + geoOf: hasGeo() ? (key) => geoOf(key) : undefined, }); db.save(state); broadcast(id, 'turn', { state, result }); diff --git a/server/map.test.ts b/server/map.test.ts index 85d627a..18a9d86 100644 --- a/server/map.test.ts +++ b/server/map.test.ts @@ -50,6 +50,8 @@ const state: GameState = { unowned: [], pendingNationOps: [], markers: [], + // (v0.28, #46) the map is ownership-only; the region-stats table is unused here. + regionStats: {}, }; // The RegionMap built from the fixture: the authoritative region key → owner source. diff --git a/server/protocol.test.ts b/server/protocol.test.ts new file mode 100644 index 0000000..3a4de86 --- /dev/null +++ b/server/protocol.test.ts @@ -0,0 +1,220 @@ +import { describe, expect, it } from 'vitest'; +import { + deriveNationStats, + migrateRegionStats, + seedRegionStats, + weightedMedian, +} from '../shared/protocol'; +import type { GameState, Nation, NationStats } from '../shared/types'; + +// --- Test fixtures ----------------------------------------------------------- + +const S = (military: number, economy: number, stability: number, population: number): NationStats => ({ + military, + economy, + stability, + population, +}); + +function nation(id: string, name: string, stats: NationStats, territory: string[]): Nation { + return { id, name, type: 'major', stats, territory, relations: {} }; +} + +function mkState( + nations: Record, + regionStats?: Record, +): GameState { + const base: GameState = { + id: 'g', + presetId: 'p', + presetTitle: 'P', + playerNationId: Object.keys(nations)[0] ?? '', + turn: 1, + date: '1939-09-01', + nations, + events: [], + pending: [], + version: 1, + unowned: [], + pendingNationOps: [], + markers: [], + regionStats: {} as GameState['regionStats'], + }; + if (regionStats !== undefined) base.regionStats = regionStats as GameState['regionStats']; + else delete (base as { regionStats?: unknown }).regionStats; // simulate a legacy row + return base; +} + +// --- weightedMedian ---------------------------------------------------------- + +describe('weightedMedian', () => { + it('returns 0 for an empty input or zero total weight', () => { + expect(weightedMedian([], [])).toBe(0); + expect(weightedMedian([5, 6], [0, 0])).toBe(0); // zero weights + expect(weightedMedian([5, 6], [NaN, -1])).toBe(0); // non-finite / non-positive → 0 + }); + + it('returns the single value when it is the only weighted input', () => { + expect(weightedMedian([42], [7])).toBe(42); + }); + + it('the median of a set of IDENTICAL values is that value (translation-invariant)', () => { + // The invariant seeding + legacy migration rely on: equal values → that value, + // regardless of the (arbitrary) weights. + expect(weightedMedian([7, 7, 7], [1, 5, 9])).toBe(7); + expect(weightedMedian([0, 0], [10, 20])).toBe(0); + expect(weightedMedian([100, 100, 100], [1, 1, 1000])).toBe(100); + // and a constant shift shifts the result by the same constant + expect(weightedMedian([107, 107, 107], [1, 5, 9])).toBe(weightedMedian([7, 7, 7], [1, 5, 9]) + 100); + }); + + it('picks the value at the first point the cumulative weight reaches half the total', () => { + // two equal weights: the lower value is reached first (cum 1 of 2 ≥ 1) + expect(weightedMedian([10, 20], [1, 1])).toBe(10); + // an odd middle: the true middle value + expect(weightedMedian([10, 20, 30], [1, 1, 1])).toBe(20); + // the heavier value dominates + expect(weightedMedian([10, 20], [1, 3])).toBe(20); + expect(weightedMedian([10, 20], [3, 1])).toBe(10); + }); + + it('treats non-finite or non-positive weights as excluded (0)', () => { + // NaN weight on 10, positive on 20 → only 20 counts + expect(weightedMedian([10, 20], [NaN, 1])).toBe(20); + // negative weight on 20, positive on 10 → only 10 counts + expect(weightedMedian([10, 20], [1, -5])).toBe(10); + }); +}); + +// --- deriveNationStats ------------------------------------------------------- + +describe('deriveNationStats', () => { + const regionStats = { + a: S(50, 50, 50, 50), + b: S(90, 90, 90, 90), + }; + const geo: Record = { a: { area: 100, population: 1000 }, b: { area: 1000, population: 1000 } }; + + it('weights by area×population: the larger region dominates the aggregate', () => { + // a: weight 100×1000=100000, b: weight 1000×1000=1000000 → b (90) dominates + const out = deriveNationStats(['a', 'b'], regionStats, (k) => geo[k]); + expect(out).toEqual(S(90, 90, 90, 90)); + }); + + it('degrades to a uniform (unweighted) median without geo: the lower value is reached first', () => { + // equal weights 1,1 → the lower (a=50) is reached at half-total first + const out = deriveNationStats(['a', 'b'], regionStats); + expect(out).toEqual(S(50, 50, 50, 50)); + // same when geoOf is provided but returns undefined for the keys (missing geo) + const out2 = deriveNationStats(['a', 'b'], regionStats, () => undefined); + expect(out2).toEqual(S(50, 50, 50, 50)); + }); + + it('skips a region key that has no stats entry', () => { + // b has no entry → only a is counted → a's (50) values + const out = deriveNationStats(['a', 'b'], { a: S(50, 50, 50, 50) }, (k) => geo[k]); + expect(out).toEqual(S(50, 50, 50, 50)); + }); + + it('returns zero stats when none of the owned regions has a stats entry', () => { + const out = deriveNationStats(['a', 'b'], {}, (k) => geo[k]); + expect(out).toEqual(S(0, 0, 0, 0)); + }); + + it('a single region yields exactly that region\'s stats (the seeding invariant)', () => { + expect(deriveNationStats(['a'], regionStats, (k) => geo[k])).toEqual(S(50, 50, 50, 50)); + expect(deriveNationStats(['a'], regionStats)).toEqual(S(50, 50, 50, 50)); + }); + + it('treats zero/negative area or population as a fallback weight of 1', () => { + // a has area 0 (weight falls back to 1×pop), b has full weight → b dominates + const g: Record = { a: { area: 0, population: 1000 }, b: { area: 1000, population: 1000 } }; + const out = deriveNationStats(['a', 'b'], regionStats, (k) => g[k]); + expect(out).toEqual(S(90, 90, 90, 90)); + }); +}); + +// --- seedRegionStats --------------------------------------------------------- + +describe('seedRegionStats', () => { + it('copies each owner\'s stats to every region it owns', () => { + const nations = { + uk: nation('uk', 'UK', S(80, 70, 60, 50), ['k1', 'k2']), + de: nation('de', 'DE', S(95, 70, 75, 90), ['k3']), + }; + expect(seedRegionStats(nations)).toEqual({ + k1: S(80, 70, 60, 50), + k2: S(80, 70, 60, 50), + k3: S(95, 70, 75, 90), + }); + }); + + it('gives the first owner\'s stats a key owned by two or more nations (corrupt)', () => { + const nations = { + a: nation('a', 'A', S(10, 10, 10, 10), ['k1']), + b: nation('b', 'B', S(90, 90, 90, 90), ['k1']), + }; + expect(seedRegionStats(nations)).toEqual({ k1: S(10, 10, 10, 10) }); + }); + + it('returns an empty table for nations with no territory', () => { + const nations = { a: nation('a', 'A', S(10, 10, 10, 10), []) }; + expect(seedRegionStats(nations)).toEqual({}); + }); +}); + +// --- migrateRegionStats ------------------------------------------------------ + +describe('migrateRegionStats', () => { + it('returns a v0.28+ row (regionStats present) UNCHANGED — same reference, no recompute', () => { + const state = mkState( + { uk: nation('uk', 'UK', S(42, 70, 60, 50), ['k1']) }, + { k1: S(99, 70, 60, 50) }, // deliberately divergent from the nation stats + ); + const out = migrateRegionStats(state); + expect(out).toBe(state); // same reference: untouched + expect(out.regionStats).toEqual({ k1: S(99, 70, 60, 50) }); + expect(out.nations.uk.stats).toEqual(S(42, 70, 60, 50)); // NOT recomputed from the region + }); + + it('migrates a legacy row (regionStats absent): seeds each owned region and preserves stats', () => { + const state = mkState({ + uk: nation('uk', 'UK', S(80, 70, 60, 50), ['k1', 'k2', 'k3']), + de: nation('de', 'DE', S(95, 70, 75, 90), ['k4']), + }); + // sanity: we built a legacy row + expect((state as { regionStats?: unknown }).regionStats).toBeUndefined(); + + const out = migrateRegionStats(state, (k) => + k === 'k1' ? { area: 100, population: 1000 } + : k === 'k2' ? { area: 1, population: 1 } + : k === 'k3' ? { area: 9, population: 9 } + : { area: 50, population: 500 }); + // each owned region is seeded with a copy of its owner's stats… + expect(out.regionStats).toEqual({ + k1: S(80, 70, 60, 50), + k2: S(80, 70, 60, 50), + k3: S(80, 70, 60, 50), + k4: S(95, 70, 75, 90), + }); + // …and the (multi-region) nation's stats are PRESERVED: the weighted median of + // identical values is that value, regardless of the (uneven) weights. + expect(out.nations.uk.stats).toEqual(S(80, 70, 60, 50)); + expect(out.nations.de.stats).toEqual(S(95, 70, 75, 90)); + // the original row is NOT mutated + expect((state as { regionStats?: unknown }).regionStats).toBeUndefined(); + expect(state.nations.uk.stats).toEqual(S(80, 70, 60, 50)); + }); + + it('a legacy 0-region nation keeps its stats (no recompute, no entries)', () => { + const state = mkState({ + uk: nation('uk', 'UK', S(80, 70, 60, 50), []), + de: nation('de', 'DE', S(95, 70, 75, 90), ['k1']), + }); + const out = migrateRegionStats(state); + expect(out.nations.uk.stats).toEqual(S(80, 70, 60, 50)); // unchanged + expect(out.nations.de.stats).toEqual(S(95, 70, 75, 90)); // single region → preserved + expect(out.regionStats).toEqual({ k1: S(95, 70, 75, 90) }); + expect('uk' in (out.regionStats ?? {})).toBe(false); + }); +}); diff --git a/server/regions.test.ts b/server/regions.test.ts index 1bc5e67..d2d5d78 100644 --- a/server/regions.test.ts +++ b/server/regions.test.ts @@ -7,8 +7,10 @@ import { allTerritoryKeys, centroidOf, expandCountriesToKeys, + geoOf, hasAdjacency, hasCentroids, + hasGeo, hasRegionData, loadRegions, neighboursOf, @@ -106,6 +108,8 @@ describe('regions (region-key loader, v0.19 #16 P1)', () => { unowned: [], pendingNationOps: [], markers: [], + // (v0.28, #46) unused here (allTerritoryKeys is ownership-only). + regionStats: {}, }; // 16 GBR sub-regions + 42 FRA sub-regions, no overlap between the two. expect(allTerritoryKeys(state).size).toBe(58); @@ -149,6 +153,21 @@ describe('regions (region-key loader, v0.19 #16 P1)', () => { expect(centroidOf('not-a-real-key-xyz')).toBeUndefined(); }); + it('(v0.28 #46) hasGeo() is true and geoOf resolves a real key to area + population', () => { + expect(hasGeo()).toBe(true); + const ain = geoOf('ain-lqtt'); + expect(ain).toBeDefined(); + expect(ain!.area).toBeGreaterThanOrEqual(0); // km² + expect(ain!.population).toBeGreaterThanOrEqual(0); + // exact spot-check values pinned from the committed manifest (Ain, FRA) + expect(ain!.area).toBeCloseTo(22557.77, 1); // km² + expect(ain!.population).toBe(2304126); + }); + + it('(v0.28 #46) geoOf returns undefined for an unknown key', () => { + expect(geoOf('not-a-real-key-xyz')).toBeUndefined(); + }); + describe('graceful degradation with no region data', () => { beforeEach(() => resetRegions()); @@ -165,6 +184,9 @@ describe('regions (region-key loader, v0.19 #16 P1)', () => { // and no centroids: the marker-placement path degrades to a no-op expect(hasCentroids()).toBe(false); expect(centroidOf('ain-lqtt')).toBeUndefined(); + // and no region-geo: the per-region stats weighting degrades to uniform + expect(hasGeo()).toBe(false); + expect(geoOf('ain-lqtt')).toBeUndefined(); }); }); @@ -219,6 +241,9 @@ describe('loadRegions resolves the manifest per environment (dev public/ vs prod // and no centroids (not copied into this dist/) -> marker placement degrades expect(hasCentroids()).toBe(false); expect(centroidOf('ain-lqtt')).toBeUndefined(); + // and no region-geo (not copied into this dist/) -> stats weighting is uniform + expect(hasGeo()).toBe(false); + expect(geoOf('ain-lqtt')).toBeUndefined(); }); it('(v0.27 #44) a dist/ WITH the centroids manifest loads them (prod layout)', () => { @@ -233,6 +258,19 @@ describe('loadRegions resolves the manifest per environment (dev public/ vs prod expect(ain!.lat).toBeCloseTo(46.16, 1); }); + it('(v0.28 #46) a dist/ WITH the region-geo manifest loads it (prod layout)', () => { + copyFileSync( + path.join(realRoot, 'public', 'region-geo.json'), + path.join(prodRoot, 'dist', 'region-geo.json'), + ); + expect(loadRegions(prodRoot)).toBe(true); + expect(hasGeo()).toBe(true); + const ain = geoOf('ain-lqtt'); + expect(ain).toBeDefined(); + expect(ain!.area).toBeCloseTo(22557.77, 1); + expect(ain!.population).toBe(2304126); + }); + it('returns false and no data when neither public/ nor dist/ holds the files', () => { const emptyRoot = path.join(base, 'empty'); mkdirSync(emptyRoot, { recursive: true }); diff --git a/server/regions.ts b/server/regions.ts index 34bb607..4f7a800 100644 --- a/server/regions.ts +++ b/server/regions.ts @@ -17,6 +17,8 @@ // field (v0.21, #16 P2) // - centroidOf(): a region key's precomputed anchor point ({ lat, lon, spread }) // — the data behind LLM-placed map markers (v0.27, #44) +// - geoOf(): a region key's area (km²) + population (from region-geo.json) +// — the weighting source for the per-region stats model (v0.28, #46) // // Deliberately turf-free: it reads the JSON files with plain JSON.parse and // never imports @turf/@types/geojson (importing one pulls @types/geojson into @@ -45,6 +47,8 @@ interface RegionData { adjacency: Record; /** region key -> anchor point (from public/region-centroids.json). */ centroids: Record; + /** region key -> { area (km²), population } (from public/region-geo.json, v0.28 #46). */ + geo: Record; } let data: RegionData | null = null; @@ -72,6 +76,7 @@ export function loadRegions(root: string): boolean { const geoFile = 'regions.geo.json'; const adjacencyFile = 'region-adjacency.json'; const centroidsFile = 'region-centroids.json'; + const regionGeoFile = 'region-geo.json'; const base = [ path.join(root, 'public'), // dev: Vite serves public/ at / path.join(root, 'dist'), // prod: Vite copies public/* -> dist/ (== clientDir) @@ -85,13 +90,13 @@ export function loadRegions(root: string): boolean { const geoPath = path.join(base, geoFile); try { const idToKey = JSON.parse(readFileSync(keyPath, 'utf8')) as Record; - const geo = JSON.parse(readFileSync(geoPath, 'utf8')) as { + const geoJson = JSON.parse(readFileSync(geoPath, 'utf8')) as { features?: Array<{ id?: string | number; properties?: { name?: string; parent?: string } }>; }; const keyToInfo: Record = {}; const keysByCountry: Record = {}; const validKeys = new Set(); - for (const f of geo.features ?? []) { + for (const f of geoJson.features ?? []) { const id = f.id == null ? '' : String(f.id); const key = idToKey[id]; if (!key) continue; // a geo feature with no manifest entry is skipped @@ -147,7 +152,40 @@ export function loadRegions(root: string): boolean { } } - data = { idToKey, keyToInfo, keysByCountry, validKeys, adjacency, centroids }; + // The region-geo manifest (v0.28, #46): area (km²) + population per region + // key — the weighting source for the per-region stats model. OPTIONAL like + // the others: a missing/broken file means "no geo data", and weighting + // degrades to uniform (every region weight 1) while everything else keeps + // working. + let geo: Record = {}; + const geoDataPath = path.join(base, regionGeoFile); + if (existsSync(geoDataPath)) { + try { + const raw = JSON.parse(readFileSync(geoDataPath, 'utf8')) as Record< + string, + { area?: unknown; population?: unknown } + >; + // Keep only well-formed entries; a corrupt file degrades to "none". + for (const [k, v] of Object.entries(raw)) { + if ( + v && + typeof v.area === 'number' && + Number.isFinite(v.area) && + v.area >= 0 && + typeof v.population === 'number' && + Number.isFinite(v.population) && + v.population >= 0 + ) { + geo[k] = { area: v.area, population: v.population }; + } + } + } catch (err) { + console.error('[regions] failed to load region-geo.json, continuing without it', err); + geo = {}; + } + } + + data = { idToKey, keyToInfo, keysByCountry, validKeys, adjacency, centroids, geo }; return validKeys.size > 0; } catch (err) { console.error('[regions] failed to load region data', err); @@ -241,6 +279,16 @@ export function hasCentroids(): boolean { return data !== null && Object.keys(data.centroids).length > 0; } +/** + * True when the region-geo manifest loaded with at least one entry (v0.28, + * #46). The server's jump route uses this to decide whether to hand the + * engine a geo resolver (which weights the per-region stats derivation by + * area×population) or fall back to a uniform (unweighted) median. + */ +export function hasGeo(): boolean { + return data !== null && Object.keys(data.geo).length > 0; +} + /** * The precomputed anchor point for a region key: the centroid of its * largest-area part, plus a `spread` (≈ 0.1 × that part's smallest bbox @@ -252,6 +300,16 @@ export function centroidOf(key: string): { lat: number; lon: number; spread: num return data?.centroids[key]; } +/** + * The precomputed area (km²) + population for a region key (v0.28, #46) — the + * weighting source for the per-region stats model. `undefined` for an unknown + * key or when no geo data is loaded (the derivation then falls back to a + * uniform weight of 1 for that region). + */ +export function geoOf(key: string): { area: number; population: number } | undefined { + return data?.geo[key]; +} + /** * The union of the region keys that share a land border with any of the given * keys — deduplicated, the queried keys themselves excluded, and sorted for diff --git a/shared/protocol.ts b/shared/protocol.ts index 7db7bd4..fc8d3b4 100644 --- a/shared/protocol.ts +++ b/shared/protocol.ts @@ -8,10 +8,12 @@ import type { GameState, MarkerType, Nation, + NationStats, RelationStatus, + RegionStats, StatKey, } from './types'; -import { MARKER_TYPES } from './types'; +import { MARKER_TYPES, STAT_KEYS } from './types'; /** A numeric change to one of a nation's 0–100 stats. */ export type StateChange = @@ -515,3 +517,137 @@ export function nationForPrompt(n: Nation) { relations: n.relations, }; } + +// --- Per-region stats (v0.28, #46) ------------------------------------------ +// +// A nation's 0–100 `NationStats` are no longer stored as independent per-nation +// values: they are DERIVED as the area×population-weighted median of the stats +// of the regions the nation owns (`GameState.regionStats`, keyed by region key). +// These pure helpers implement that derivation, the fresh-game / legacy seeding, +// and the one-time migration of a pre-v0.28 row. They have no runtime deps. + +/** + * The weighted median of `values` with per-value `weights` (paired by index). + * The pairs are sorted by value ascending and the result is the value at the + * first point where the cumulative weight reaches at least half the total + * weight. An empty input, or a total weight of zero, yields 0. Non-finite or + * non-positive weights are treated as 0 (excluded from the median). + * Translation-invariant: adding a constant to every value shifts the result by + * that constant — so the median of a set of **identical** values is that value + * (the invariant the equal-value seeding and legacy migration rely on). + * Pure + shared. + */ +export function weightedMedian(values: number[], weights: number[]): number { + if (values.length === 0) return 0; + const pairs = values + .map((v, i) => { + const w = + typeof weights[i] === 'number' && Number.isFinite(weights[i]) && weights[i] > 0 + ? weights[i] + : 0; + return { v: Number.isFinite(v) ? v : 0, w }; + }) + .filter((p) => p.w > 0); + if (pairs.length === 0) return 0; + const total = pairs.reduce((s, p) => s + p.w, 0); + if (total <= 0) return 0; + pairs.sort((a, b) => a.v - b.v); + let cum = 0; + for (const p of pairs) { + cum += p.w; + if (cum >= total / 2) return p.v; + } + return pairs[pairs.length - 1]!.v; +} + +/** + * Derive a nation's aggregate {@link NationStats} from the stats of the regions + * it owns: each of the four stats is the **area×population-weighted median** + * (see {@link weightedMedian}) over the nation's owned region keys. A region's + * weight is `(area > 0 ? area : 1) × (population > 0 ? population : 1)` — both + * factors fall back to 1 when the region has no geo entry (`geoOf` returns + * `undefined` for it), so every region carries weight ≥ 1 and a missing geo + * degrades gracefully to a uniform (unweighted) median. A region key with no + * `regionStats` entry is skipped. Returns the zero stats when the nation owns + * no region with a stats entry (the caller's 0-region guard keeps the nation's + * directly-maintained `n.stats` in that case). Pure + shared. + */ +export function deriveNationStats( + regionKeys: string[], + regionStats: Record, + geoOf?: (key: string) => { area: number; population: number } | undefined, +): NationStats { + const zero: NationStats = { military: 0, economy: 0, stability: 0, population: 0 }; + const byStat: Record = { + military: [], + economy: [], + stability: [], + population: [], + }; + const weights: number[] = []; + for (const key of regionKeys) { + const rs = regionStats[key]; + if (!rs) continue; // a key with no per-region stats is skipped + const g = geoOf?.(key); + const a = g && g.area > 0 ? g.area : 1; + const p = g && g.population > 0 ? g.population : 1; + weights.push(a * p); + for (const k of STAT_KEYS) byStat[k].push(rs[k]); + } + if (weights.length === 0) return zero; + const out: NationStats = { military: 0, economy: 0, stability: 0, population: 0 }; + for (const k of STAT_KEYS) out[k] = clampStat(weightedMedian(byStat[k], weights)); + return out; +} + +/** + * Seed a per-region stats table from a set of nations (used both at game + * creation and by the legacy migration): every owned region key gets a **copy** + * of its owner's current `NationStats`, so a fresh nation's regions all start + * identical (the weighted median of identical values is that value, so the + * derived `n.stats` equals the seeded one). A region with two or more owners + * (corrupt) is seeded by the first owner encountered; a key owned by nobody gets + * no entry. Pure + shared. + */ +export function seedRegionStats(nations: Record): Record { + const out: Record = {}; + for (const n of Object.values(nations)) { + for (const key of n.territory) { + if (!(key in out)) out[key] = { ...n.stats }; // first owner wins (sole ownership holds normally) + } + } + return out; +} + +/** + * (v0.28, #46) One-time migration of a **legacy** (pre-v0.28) `GameState` row + * into the per-region-stats model, returning a new state (the input is never + * mutated): + * - a row that already has `regionStats` (a v0.28+ save) is returned + * **UNCHANGED** — its `Nation.stats` are already the correct derived medians + * and must not be recomputed on load (that would drift them); + * - a legacy row (`regionStats` absent at runtime) seeds every owned region key + * with a copy of its owner's current stats (so each nation's regions start + * identical), then recomputes each nation's stats as the weighted median of + * its regions (a 0-region nation keeps its stats). Without `geoOf` the + * weighting is uniform, which is weight-invariant for the identical-value + * case, so the recomputed stats equal the originals — the migration is a + * no-op on the visible numbers and only materializes the (previously + * implicit) per-region values. Never throws. + */ +export function migrateRegionStats( + state: GameState, + geoOf?: (key: string) => { area: number; population: number } | undefined, +): GameState { + // A v0.28+ row already carries per-region stats: leave it exactly as saved. + if (state.regionStats !== undefined) return state; + // Legacy row: materialize per-region stats from each nation's (uniform) stats. + const next: GameState = { ...state, regionStats: seedRegionStats(state.nations) }; + for (const n of Object.values(next.nations)) { + if (n.territory.length === 0) continue; // 0-region guard: keep its stats + if (n.territory.some((k) => next.regionStats[k])) { + n.stats = deriveNationStats(n.territory, next.regionStats, geoOf); + } + } + return next; +} diff --git a/shared/types.ts b/shared/types.ts index 5e13772..1c35b7c 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -13,6 +13,19 @@ export interface NationStats { population: number; } +/** + * (v0.28, #46) The 0–100 status indices of a **single region**. Per-region stats + * are the unit of record: a nation's aggregate {@link NationStats} is derived as + * the area×population-weighted median of the stats of the regions it owns (see + * `protocol.deriveNationStats`). Every value is clamped to 0–100 by the engine. + */ +export interface RegionStats { + military: number; + economy: number; + stability: number; + population: number; +} + export const STAT_KEYS = ['military', 'economy', 'stability', 'population'] as const; export type StatKey = (typeof STAT_KEYS)[number]; @@ -217,6 +230,19 @@ export interface GameState { * server/db.ts) — additive, no data wipe. Capped at protocol.MAX_MARKERS. */ markers: MapMarker[]; + /** + * (v0.28, #46) Per-region stats, keyed by **region key** (see + * `public/region-keys.json`): each owned region carries its own four 0–100 + * stats, and a nation's aggregate `Nation.stats` is the area×population- + * weighted median of the regions it owns (see `protocol.deriveNationStats`). + * A region that ends a turn **unowned** (released to no man's land) has NO + * entry, and a preset-unassigned region (never owned) has none until first + * claimed (re-claiming re-inits it to the gainer's stats). Always present on a + * fresh game (every owned region seeded with its owner's initial stats); a + * legacy SQLite row without the field is migrated at load (see server/db.ts) + * — additive, no data wipe. + */ + regionStats: Record; } /** -- 2.54.0