Skip to content

Add Bangladesh city corporation and sub-district admin boundaries - #33

Merged
mberg merged 23 commits into
masterfrom
bd-city-corporation-boundaries
Jul 5, 2026
Merged

Add Bangladesh city corporation and sub-district admin boundaries#33
mberg merged 23 commits into
masterfrom
bd-city-corporation-boundaries

Conversation

@mberg

@mberg mberg commented Jul 5, 2026

Copy link
Copy Markdown
Member

Summary

  • Extends admin_boundaries with an additive, self-referential tree (parent_id/boundary_type/source_code) so city corporations, zones, wards, and rural blocks can attach as children of existing division/district/upazila/union rows without touching any existing column, row, or query path.
  • Adds a shapefile importer for the real Bangladesh district and city-corporation shapefiles in data/new/ (21 districts, 8 city corporations), resolving parents by name with a small alias table and a spatial overlap sanity check.
  • Generalizes get_admin_boundary_children, add_campaign_area, and list_campaign_areas to work with the new boundary types alongside the legacy pcode-based ones.
  • Adds a scoped pixel-population script for the new boundary levels (not yet run against full production-scale data - see test plan).
  • Adds a new drill-down picker (AdminBoundaryDrillPicker) so a city corporation (or any new boundary type) can be picked directly as a campaign area's starting point, alongside the existing map-click flow.

Design doc: docs/superpowers/specs/2026-07-03-bd-city-corporation-boundaries-design.md
Implementation plan: docs/superpowers/plans/2026-07-03-bd-city-corporation-boundaries.md

Explicitly out of scope (documented, not touched): Stratified Cluster Sampling generalization for city corporations (separate follow-up plan), regenerating the static PMTiles map-click layer, District.json.

Test plan

  • All 11 plan tasks implemented with TDD, each passed an independent task-level code review
  • Final whole-branch review passed - no Critical issues
  • Real shapefile import run against data/new/ (21 districts, 8 city corporations) into local dev DB
  • npx tsc --noEmit clean across the frontend
  • Run scripts/populate_boundary_pixels_for_new_levels.py against production data before relying on admin_boundary_pixels for the new levels (pixel computation for campaign areas already works without this - it recomputes directly from geometry)
  • Manual browser click-through of the new drill-down picker (not yet performed - Clerk-auth-gated, no test credentials available in the dev sandbox)

mberg added 23 commits July 3, 2026 08:27
…mpling

create_replacement_pixels could silently lose a candidate to a concurrent
claim (INSERT ... ON CONFLICT ... DO UPDATE ... WHERE no-op) while still
reporting success; it now retries remaining candidates and only counts an
actual claim. Building-target rounds previously had no replacement-pixel
logic at all - a new activity anchors each sampled building's quadkey to a
coverage_pixel row so the existing neighbor-selection logic can be reused.
Both round-creation UIs get an explicit "generate replacement pixels"
toggle and hints for the building/population count check, and the map
legend now explains the primary/replacement pixel colors.
…ies spec

Verified geopandas (already a dependency) reads .shp natively, so no new
pyshp dependency is needed. Also found the map-click add-area flow is
backed by separate static PMTiles infrastructure, not the admin_boundaries
table this spec extends - documented as out of scope, with the drill-down
picker as the actual extension point.
Union names are only unique within an upazila, not district-wide, so
grouping solely by UNINAME could merge two different upazilas' same-named
unions into one group and attach one upazila's wards/blocks under the
wrong upazila's matched union row. Group by (THANAME, UNINAME) instead,
and key the union_id_cache the same way.
TestImportCityCorporation relied on the real district name 'Dhaka',
which now exists in the shared dev database after the CLI import ran
against real shapefiles. import_city_corporation's idempotency check
found the existing DNCC row and returned 0 created instead of 1,
breaking test_creates_city_corporation_zones_and_wards.

Add a district_fixture that inserts a fresh, uniquely-named district
per test (mirroring TestImportRuralDistrict's union_fixture), and
point synthetic_dncc_gdf's DISTNAME at it so these tests no longer
depend on real seeded data existing or not existing in the database.
Districts can have both a pcode-reachable upazila set and a
parent_id-linked city corporation (e.g. Chittagong, Dhaka, Gazipur,
Khulna, Narayanganj, Rangpur, Sylhet) - these are siblings, not
alternatives. The children endpoint returned early when parent_id
children existed, silently dropping the real upazilas for those
districts. Now both lookups always run and their results are merged.

Also fixes a latent bug in the pcode fallback: the parent's own pcode
was picked as the first non-null adm*_pcode column rather than the
one matching its actual level, so districts with populated ancestor
codes (all real imported districts) resolved the wrong pcode and
silently returned zero pcode-based children. Replaced the fragile
dash-counting UUID heuristic with an actual uuid.UUID() check, and
dropped the unused boundary_type column from the parent_id children
query.
…ssage

The merge test's district fixture only had adm2_pcode set, which happened
to produce the same result under both the old "first non-null pcode
column" heuristic and the fixed parent_row[1 + parent_level] lookup,
so it couldn't catch a regression back to the old heuristic. Updated the
fixture to populate adm0/adm1/adm2_pcode together, matching real district
rows, and verified (via a temporary local revert) that the old heuristic
now fails this test.

Also restored the 'No child level exists' message for level-4 boundaries
with neither parent_id nor pcode-based children, which was silently
dropped when the parent_id and pcode lookups were merged.
The existing tests only replayed the by-id and by-pcode SQL by hand,
never calling add_campaign_area itself, so a typo in its if/else
branching (checking the wrong variable, reversed branches) would pass
unnoticed. Add tests that call add_campaign_area.__wrapped__ directly
with a real Flask request context, a committed campaign, and a real
admin boundary with no pcode (mirroring city corporations), verifying
both the admin_boundary_id and pcode paths create the correct
campaign_areas row. Confirmed the new tests fail when the branch
condition is broken.
Add optional boundary_ids parameter to populate_pixels_for_leaf_boundaries
and roll_up_pixels_to_ancestors so callers can target specific rows instead
of always processing every matching row in admin_boundaries. Defaults to
None (process everything), preserving existing full-run behavior.

Scope test calls to their own synthetic boundary ids so tests don't trigger
a spatial join against the ~21,000+ real committed leaf boundaries and the
full pixels table already present in the shared database.

Also fix a test assertion that assumed a single spatial-join match; real
committed pixel data in the same region means multiple rows can legitimately
intersect, so check membership instead of exact equality.
@mberg
mberg merged commit c8a7441 into master Jul 5, 2026
3 of 4 checks passed
@mberg
mberg deleted the bd-city-corporation-boundaries branch July 5, 2026 02:20

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d65c150d14

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

""", (admin_boundary_pcode, admin_boundary_pcode, admin_boundary_pcode,
admin_boundary_pcode, admin_boundary_pcode))
# Get geometry, name, id, and pre-computed stats from admin_boundaries table by id or pcode
if admin_boundary_id_param:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle id-only boundaries in pixel sampling

When admin_boundary_id_param is used for the new city-corporation/zone/ward/block rows, those imported rows do not have adm*_pcode values, but they can now be saved as campaign areas. The pixel-sampling workflow still calls sample_pixels_for_campaign_area, which returns No admin pcode found for campaign area before sampling from pixel_area, so an area added through the new drill-down path cannot be adaptively pixel-sampled even after its pixels are populated.

Useful? React with 👍 / 👎.

quadkey, campaign_id, indicator_id, version, n_trials, n_covered, rounds
)
VALUES (%s, %s, %s, 0, 0, 0, ARRAY[%s])
ON CONFLICT (quadkey, indicator_id, campaign_id) DO UPDATE SET

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Match the coverage_pixel conflict key

For building-target sampling with replacements enabled, this new upsert runs before replacement pixels are created. The checked schema defines the coverage-pixel uniqueness with version included (UNIQUE (quadkey, indicator_id, campaign_id, version) in migrations_campaign_refactor.py), so PostgreSQL rejects ON CONFLICT (quadkey, indicator_id, campaign_id) with no matching unique constraint, causing the default replacement-enabled building sampling path to fail.

Useful? React with 👍 / 👎.

Comment on lines +155 to +159
if generate_replacements:
self.status = "creating_replacements"
primary_pixel_ids = await workflow.execute_activity(
ensure_coverage_pixels_for_locations,
args=[campaign_id, indicator_id, selected_ids, self.round_number],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear pixel anchors when resampling buildings

When generate_replacements is true for building-target sampling, the workflow now creates coverage_pixel rows for the selected buildings' quadkeys and replacement rows, but the resample branch above only calls clear_round_from_buildings. Resampling a building round therefore leaves the old pixel round assignments/replacements behind, so maps and later replacement selection can still treat the previous building pixels as sampled even after the building sample has been replaced.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant