← gpu-pilot · json · source: crosswalk/CROSSWALK.md

Block-Group ↔ H3 Crosswalk + hex_hta

TIGER 2022 geometries → 22M-cell crosswalk → hex-level CNT H+T view in the commute DB — closes the long-flagged blkgrp↔H3 item. · updated 2026-06-12 06:25

TL;DR

  • 3 new objects in the commute DB: tiger_blkgrp (35,559 CT/NJ/NY/PA block groups, TIGER 2022 = 2020 delineation, MultiPolygon 4326), blkgrp_h3_crosswalk (22,018,397 rows — res 9: 2.75M cells, res 10: 19.27M; PK (h3,res), centroid containment so each cell maps to exactly ONE block group), view hex_hta (h3 → ht_ami/h_ami/t_ami/transit_cost/vmt/autos/% transit + cbsa_name).
  • Coverage validated both ways: 100.00% of the 1,310,600 nyc hex_demographics res-10 cells get a block group; 99.4–100% of BGs get a res-10 cell per state (3,254 NYC-scale micro-BGs miss at res 9, 97% recover at res 10; only 98 statewide miss both).
  • Spot checks bit-exact pg↔h3-py: Times Sq → 360610119001 (ht 12%, t 5%, $2,736/yr transit — hyper-transit Manhattan); Piscataway → 340230006031 (ht 57%, $3/yr transit — car suburbia). Household-weighted avg ht_ami over the nyc cells = 43.0, matching the CBSA row exactly.
  • Fast + cheap: whole build ≈ 9 min, 2.55 GB (h3_postgis extension's h3_polygon_to_cells at ~60K cells/s). The 72% match on the RAW res-9 bbox grid is the grid's Atlantic rectangle — 92% of unmatched are >5 km offshore.
  • National rollout is the same loop ×47 states: ≈ 91M cells res-9-only (~11 GB, ~30 min) or 727M both-res (~84 GB, ~4 h single-stream); recommendation res 9 everywhere + res 10 in CBSA counties ≈ 25 GB. Alaska needs an antimeridian split. Bulk alt: IPUMS NHGIS national shapefile.

Block-Group ↔ H3 Crosswalk + hex-level CNT H+T (hex_hta)

Built: 2026-06-12 on geocoder → commute DB (gtfs3, 172.26.1.152:5433) Status: SHIPPED — closes the long-flagged "blkgrp↔H3 needs TIGER geometries" item from the CNT H+T load. Scope: CT (09), NJ (34), NY (36), PA (42) — the four states of the CNT NYC CBSA (NY-NJ-PA delineation incl. Pike County PA) plus their full state extents, res 9 + res 10. → NATIONAL since 2026-06-12 (overnight rollout): all 50 states + DC at res 9; res 10 in every CBSA county. See "NATIONAL ROLLOUT" section at the bottom — totals there supersede the 4-state numbers below.

What this gives you

Any H3 cell (res 9 or 10) in NY/NJ/CT/PA now joins to its 2020-census block group and, through it, to every CNT H+T 2022 metric. One view does it:

select * from hex_hta where h3 = '892a100d67bffff' and res = 9;
-- h3, res, blkgrp_geoid, ht_ami, h_ami, t_ami, t_cost_ami, transit_cost_ami,
-- autos_per_hh_ami, vmt_per_hh_ami, pct_transit_commuters_ami, cbsa_name

This is the missing piece that lets the GPU RAPTOR hex products (res-10 NYC commute maps, TLCcosts H+U+C) pull CNT affordability context per hex with zero geometry work at query time.

Objects created (commute DB)

Object Type Rows Notes
tiger_blkgrp table 35,559 2020-delineation BGs, geometry(MultiPolygon,4326), GiST indexed, 100 MB
blkgrp_h3_crosswalk table 22,018,397 PK (h3, res) + index on blkgrp_geoid, 2,551 MB; res 9 = 2,752,305 cells / 32,305 BGs, res 10 = 19,266,092 cells / 35,461 BGs
hex_hta view res 9: 2,575,615 · res 10: 18,029,119 crosswalk ⋈ hta_index (year=2022, level='blkgrp'); 93.6% of cells carry a CNT row (the rest sit in zero-household BGs CNT doesn't publish)
h3_postgis extension installed 2026-06-12 (CASCADE pulled postgis_raster); provides h3_polygon_to_cells(geometry,res)

Everything else (hta_index, hex_demographics, …) was touched read-only.

Sources

  • Geometries: Census TIGER/Line 2022 block groups — https://www2.census.gov/geo/tiger/TIGER2022/BG/tl_2022_{09,34,36,42}_bg.zip (no API key; TIGER2022+ carries the 2020 delineation, which is exactly what CNT H+T 2022 GEOIDs use). Zips archived in crosswalk/raw/ (151 MB). Native CRS NAD83 (EPSG:4269) → ST_Transform → 4326 at load.
  • Attributes: hta_index (CNT H+T 2022, loaded 2026-06-12; 239,178 blkgrp rows national; 35,300 in these four states — 259 TIGER BGs have no CNT row, the usual zero-household BGs: parks, water, airports, prisons).

Shapefile feature counts (match TIGER row-for-row): CT 2,717 · NJ 6,599 · NY 16,070 · PA 10,173 = 35,559, all valid geometries, 0 ST_MakeValid needed.

Method

  1. load_tiger_blkgrp.py (geopandas/pyogrio in the gpu-pilot venv) → CSV with NAD83 WKB hex (5.8 s for all four states).
  2. sql/01_tiger_blkgrp.sql — temp-stage \copy, then ST_Multi(ST_Transform(ST_SetSRID(geom,4269),4326)) into tiger_blkgrp (~7 s).
  3. sql/02_crosswalk.sqlh3_polygon_to_cells(geom, res) from h3-pg 4.2.3's h3_postgis extension. H3 v4 polygon-to-cells is centroid containment: a cell belongs to the BG that contains its center, so each cell maps to exactly one block group (PK (h3,res) enforces it; ON CONFLICT DO NOTHING absorbs degenerate boundary ties). res 9 ≈ 0.105 km²/cell, res 10 ≈ 0.015 km²/cell.
  4. sql/03_hex_hta.sql — plain view; hta_index join keys (geoid, year=2022, level='blkgrp').
  5. sql/04_validate.sql — coverage both directions + spot checks (results below).

The pg↔python H3 agreement was verified: h3_latlng_to_cell(point(lng,lat),res) in h3-pg returns bit-identical cells to h3-py 4.5 (892a100d67bffff for Times Square res 9). h3-pg native point is (x=lng, y=lat) — PostGIS axis order, not lat-first.

Validation results (sql/04_validate.sql, full output in validate_run.log)

Forward (cell → block group):

  • hex_demographics nyc (all res 10, the product grid): 1,310,600 / 1,310,600 = 100.00 % matched. 1,310,563 (99.997 %) also carry a CNT row through hex_hta.
  • Raw nyc_tristate_h3_res9.parquet grid (535,471 cells, unfiltered bbox grid): 385,678 = 72.03 % matched. The 149,793 unmatched are the grid's ocean rectangle, not a defect: every one sits at lat ≤ 40.78 (the grid's southern Atlantic band); 86.6 % are open Atlantic south of LI/NJ, 512 in the Delaware Bay corner. In a 2,000-cell sample, 92.4 % lie > 5 km from any block group, 7.0 % are the 0.5–5 km offshore fringe, 0.65 % within 500 m of shore (centers just offshore). Land-cell coverage is effectively complete.

Reverse (block group → ≥1 cell):

state BGs with res-9 cell with res-10 cell
09 CT 2,717 2,701 (99.4 %) 2,717 (100 %)
34 NJ 6,599 6,386 (96.8 %) 6,599 (100 %)
36 NY 16,070 13,276 (82.6 %) 15,972 (99.4 %)
42 PA 10,173 9,942 (97.7 %) 10,173 (100 %)

3,254 BGs have no res-9 cell (NYC's tiny 1–2-block BGs are smaller than a 26-acre res-9 hex); 3,156 of them (97 %) recover at res 10. 98 BGs — all NY, all micro-BGs smaller than a 3.7-acre res-10 hex — have no cell at either res; their territory is absorbed by neighboring BGs' cells, which is the correct centroid-containment behavior for hex products (use res 11 or point-on-surface assignment if a complete BG→cell map is ever needed).

Spot checks (h3-pg h3_latlng_to_cell verified bit-identical to h3-py):

  • Times Square (40.758, −73.9855) → res 9 892a100d67bffff → BG 360610119001 (Manhattan 36061 ✓): ht_ami 12 %, t_ami 5 %, transit_cost_ami $2,736/yr, 30 % transit commuters — the classic hyper-transit Manhattan profile. res 10 lands in 360610119002 with the same shape (ht 10/t 5/$2,765).
  • Piscataway NJ (40.5478, −74.4734) → 892a1069a97ffff → BG 340230006031 (Middlesex 34023 ✓): ht_ami 57 %, t_ami 18 %, transit_cost_ami $3/yr, 1 % transit commuters — car suburbia, exactly as expected.

CBSA sanity: plain per-cell avg ht_ami over the 1.31M nyc cells = 55.2 (area-weighted — exurban acreage dominates cell counts). Household-weighted over the same cells' BGs = 43.0, matching the CBSA row's ht_ami = 43 exactly. t_ami: cells 18.0 vs CBSA 13 (same area-vs-household effect).

Integrity: 0 crosswalk geoids missing from tiger_blkgrp; 93.58 % of cells (both res) join a CNT blkgrp row.

Runtime

Step Wall time
TIGER download (4 zips, 59 MB) ~15 s
Shapefile → CSV 5.8 s
Load tiger_blkgrp (transform + GiST) ~7 s
Crosswalk res 9 (2,752,305 cells) 57.9 s
Crosswalk res 10 (19,266,092 cells) 5 m 10 s (+13.7 s geoid index)
Validation suite ~1.5 min
Total ≈ 9 min end to end

How to extend nationally (the remaining 47 states)

The loop is state-FIPS-parameterized end to end; nothing in the SQL is region-specific:

  1. curl https://www2.census.gov/geo/tiger/TIGER2022/BG/tl_2022_{fips}_bg.zip for the other 47 states + DC (+ 72 PR if wanted — CNT covers PR partially). Total national TIGER BG set ≈ 242K BGs, ~1.1 GB of zips.
  2. load_tiger_blkgrp.py <fips> … → CSV → same 01 SQL (append new \copy lines or loop).
  3. Re-run the two crosswalk INSERTs — they're idempotent (ON CONFLICT DO NOTHING), only new geoids generate new cells. Parallelize by running one psql per state list if wanted; the inserts don't contend (different h3 ranges).
  4. hex_hta needs no change — hta_index is already national (239,178 blkgrp rows).

Measured basis: 4 states = 297,526 km² of BG territory → 22.0M cells in 6.1 min of polyfill+insert (≈ 60K cells/s single psql stream), 2,551 MB on disk.

National (50 states + DC ≈ 9.83 M km², ×33 area scale):

res 9 only res 9 + 10
crosswalk rows ≈ 91 M ≈ 727 M
crosswalk size ≈ 10–11 GB ≈ 84 GB
polyfill+insert (1 stream) ≈ 32 min ≈ 3.5–4 h
polyfill+insert (4 parallel state batches) ≈ 10 min ≈ 1–1.5 h

Plus: 51 TIGER zips ≈ 1.1 GB (~10 min download), national tiger_blkgrp 242,335 BGs ≈ 700 MB (~2 min load), final PK/geoid indexes ≈ 15 min at res-10 volume. Practical recommendation: res 9 everywhere + res 10 only in CBSA counties (where the hex products live) keeps it ≈ 25 GB.

Alaska gotcha for the rollout: the Aleutians cross the antimeridian; H3 polyfill mishandles transmeridian polygons unless they're split at ±180° first (ST_Split or shift-lon trick) — handle statefp 02 specially or polyfill it with h3-py's transmeridian-aware path.

Bulk alternative: IPUMS NHGIS API (key at ~/.ipums_api_key) serves single national block-group shapefiles per vintage (https://api.ipums.org/extracts/ with {"shapefiles":["us_blck_grp_2022_tl2022"]} — one ~900 MB national file instead of 51 zips). Useful if Census FTP throttles; same schema, same loader.

Files

crosswalk/
├── CROSSWALK.md              ← this doc
├── load_tiger_blkgrp.py      ← shapefile → staging CSV (WKB hex, NAD83)
├── raw/tl_2022_{09,34,36,42}_bg[.zip|/]   ← TIGER source of record
├── staging/                  ← CSVs (regenerable; safe to delete)
├── crosswalk_build.log       ← timed psql output of the 02 build
└── sql/
    ├── 01_tiger_blkgrp.sql   ← DDL + load
    ├── 02_crosswalk.sql      ← h3_postgis polyfill, res 9 + 10
    ├── 03_hex_hta.sql        ← the view
    └── 04_validate.sql       ← validation suite (rerunnable)

NATIONAL ROLLOUT — 2026-06-12 (overnight, geocoder → gtfs3)

Extended the crosswalk from 4 states to all 50 states + DC per the rollout plan above. Same method, same two tables, zero schema changes; hex_hta view needed no change.

What changed

Object Before After
tiger_blkgrp 35,559 BGs (4 states), 100 MB 239,781 BGs (51 states), 1,179 MB, 0 invalid geoms
blkgrp_h3_crosswalk res 9 2,752,305 cells / 32,305 BGs 92,331,633 cells / 235,066 BGs (full national extent)
blkgrp_h3_crosswalk res 10 19,266,092 cells / 35,461 BGs 310,904,941 cells / 223,177 BGs (CBSA counties + the 4 original full states)
blkgrp_h3_crosswalk total 22,018,397 rows, 2,551 MB 403,236,574 rows, 45 GB
commute DB size 4,313 MB 48 GB (Δ ≈ +44 GB)

Disk pressure: none — PG data dir is /data/postgres/main on a 1.8 TB NVMe with 1.6 TB still free (the earlier "df -h /" check was looking at the wrong mount; root barely moved).

Note: the doc's earlier "≈242,335 BGs national" was an estimate; the actual TIGER2022 national BG count is 239,781 — per-state DB counts match the shapefile feature counts exactly, all 51.

res-10 scope decision (documented choice)

res 10 was built ONLY for counties inside a CBSA, derived read-only from hta_index: level='county' AND year=2022 AND cbsa_name non-empty AND cbsa_name matches a real level='cbsa' row → 1,836 counties (metro + micropolitan). The 6 CT planning regions whose cbsa_name is a comma-separated multi-CBSA list (the known cbsa_name-list gotcha: 09110–09190) were excluded by that filter — harmless, CT is already full-state res 10 from the 4-state build. Verified post-build: 1,836 / 1,836 CBSA counties have res-10 cells (100%). Heads-up: metro+micro CBSA counties cover 4.5M km² (~half the lower 48), which is why res 10 came in at 311M rows — well above the doc's old "≈25 GB total" ballpark; actual total is 45 GB. Fine on /data.

Alaska disposition

AK has exactly one transmeridian BG: 020160001001 (Aleutians West, 28 parts, bbox −179.23…179.86). sql/05_ak_res9.sql polyfills AK in two passes: (1) the 503 normal BGs straight through; (2) the transmeridian BG clipped into west/east lobes with two envelopes (−180,45,−128,76) and (165,45,180,76), ST_Dump per lobe, polyfill per part — 294,172 cells in 6 s. Nothing was skipped: 504/504 AK BGs have res-9 cells (17,532,608 total). AK's 5 CBSA counties (Anchorage, Mat-Su, Fairbanks, Juneau, Ketchikan) are all far east of the antimeridian, so res 10 took the plain path. AK was the res-9 long pole: 23.1 min (giant North Slope BGs).

Runtime (wall clock, 2026-06-12 UTC)

Step Wall time
Download 47 TIGER zips (1.0 GB) + extract ~8 min (4-way parallel curl, 0 failures)
Shapefile → CSV staging (47 states, geopandas) ~1 min
Load tiger_blkgrp (+204,222 BGs, transform + per-row GiST upkeep) 17 min (one INSERT — index already existed; drop/rebuild would be faster next time)
res-9 polyfill, 46 states (3 parallel streams, 1 txn/state) 15.4 min (05:00:51→05:16:15)
res-9 AK special path (concurrent with pool) 23.1 min — res-9 done 05:23:56
res-10 polyfill, 47 states × CBSA counties (3 streams, 1 txn/state) 52.1 min (05:19:57→06:12:02, overlapped AK tail)
ANALYZE + validation suite (sql/06_validate_national.sql) ~10 min
Total downloads→validated ≈ 1 h 50 m

Aggregate insert throughput ≈ 60–120K cells/s over 381M new rows with both indexes live. Failures: zero — all 47 downloads, 47 state loads, 47 res-9 txns, 47 res-10 txns, AK both passes OK. (Logging nit: drivers used psql -qAt, which suppresses INSERT tags, so per-state row counts in res9_build.log/res10_build.log are blank — counts below were pulled from the DB afterwards.)

Validation (full output: validate_national.log)

  • Area sanity: per-state res-9 cells vs (aland+awater)/0.105 ratio ∈ [0.87, 1.12] for all 51 — no outliers.
  • Reverse coverage (BG → ≥1 res-9 cell): ≥99% in 41 states; the only sub-96% are the known micro-BG cases — DC 88.4%, HI 92.7%, NY 82.6% (NYC's 1–2-block BGs, unchanged from 4-state build).
  • hex_hta spot checks (new states): Chicago Loop → 170318391001 (ht 40/t 8), LA downtown → 060372073061 (ht 30/t 11), Seattle → 530330082001 (ht 35/t 9), Denver + Miami resolve with correct CBSAs (their BGs have NULL ht_ami in CNT itself).
  • CNT-join coverage: 97.45% of res-9 and 96.21% of res-10 cells carry a CNT blkgrp row (better than the 4-state build's 93.6%); hex_hta now serves 89,974,921 res-9 + 299,122,249 res-10 CNT-joined cells.
  • Integrity: 0 crosswalk geoids missing from tiger_blkgrp.

Per-state row counts

st state BGs res-9 cells res-10 cells res-10 scope
01 AL 3,925 1,300,903 6,190,693 45 CBSA counties
02 AK 504 17,532,608 8,629,578 5 CBSA counties
04 AZ 4,773 2,442,815 14,457,203 12 CBSA counties
05 AR 2,294 1,244,567 4,897,154 41 CBSA counties
06 CA 25,607 3,773,893 20,277,539 45 CBSA counties
08 CO 4,058 2,346,179 7,422,186 29 CBSA counties
09 CT 2,717 133,614 935,262 full state (orig)
10 DE 706 62,885 440,186 3 CBSA counties
11 DC 571 1,730 12,104 1 CBSA county
12 FL 13,388 1,887,880 11,005,303 51 CBSA counties
13 GA 7,446 1,562,190 7,172,838 105 CBSA counties
15 HI 1,083 234,812 1,635,777 4 CBSA counties
16 ID 1,284 2,075,362 7,472,923 27 CBSA counties
17 IL 9,898 1,449,801 6,788,647 63 CBSA counties
18 IN 5,290 916,225 5,061,830 72 CBSA counties
19 IA 2,703 1,391,130 3,840,595 38 CBSA counties
20 KS 2,461 1,898,585 4,475,545 37 CBSA counties
21 KY 3,581 1,047,770 3,822,585 63 CBSA counties
22 LA 4,294 1,202,056 6,475,486 46 CBSA counties
23 ME 1,184 809,220 1,270,692 6 CBSA counties
24 MD 4,079 313,713 1,950,383 21 CBSA counties
25 MA 5,116 252,316 1,714,545 13 CBSA counties
26 MI 8,386 2,232,901 8,493,314 49 CBSA counties
27 MN 4,706 2,033,879 7,745,560 46 CBSA counties
28 MS 2,445 1,153,644 4,765,013 46 CBSA counties
29 MO 5,031 1,690,003 5,640,353 57 CBSA counties
30 MT 900 3,745,175 4,617,791 10 CBSA counties
31 NE 1,648 1,846,756 3,490,705 29 CBSA counties
32 NV 1,963 2,558,273 11,716,621 11 CBSA counties
33 NH 997 217,859 1,363,433 9 CBSA counties
34 NJ 6,599 215,571 1,509,075 full state (orig)
35 NM 1,614 2,602,528 12,334,520 23 CBSA counties
36 NY 16,070 1,282,170 8,975,228 full state (orig)
37 NC 7,111 1,469,185 7,857,388 75 CBSA counties
38 ND 632 1,692,565 3,485,329 13 CBSA counties
39 OH 9,472 1,107,351 6,426,457 71 CBSA counties
40 OK 3,374 1,571,270 5,203,117 37 CBSA counties
41 OR 2,970 2,511,830 11,167,387 26 CBSA counties
42 PA 10,173 1,120,950 7,846,527 full state (orig)
44 RI 792 37,297 261,131 5 CBSA counties
45 SC 3,408 888,390 4,854,462 34 CBSA counties
46 SD 694 1,899,489 3,624,819 21 CBSA counties
47 TN 4,562 1,077,748 5,147,912 64 CBSA counties
48 TX 18,638 5,793,342 20,395,067 131 CBSA counties
49 UT 2,020 1,924,014 6,149,162 15 CBSA counties
50 VT 552 222,223 889,337 8 CBSA counties
51 VA 5,963 1,116,841 4,588,157 89 CBSA counties
53 WA 5,311 1,947,069 9,956,587 28 CBSA counties
54 WV 1,639 618,094 2,253,421 31 CBSA counties
55 WI 4,692 1,536,750 5,674,021 40 CBSA counties
56 WY 457 2,338,212 8,523,993 11 CBSA counties
Total 239,781 92,331,633 310,904,941

New files from the rollout

crosswalk/
├── download_national.sh            ← 47-state TIGER fetch + extract (idempotent)
├── build_res9_national.sh          ← res-9 driver: 1 txn/state, 3 parallel psql streams
├── build_res10_cbsa.sh             ← res-10 driver: CBSA-county filter read from hta_index
├── resume_res9.sh                  ← idempotent resume helper (unused — zero failures)
├── res9_build.log / res9_ak.log / res10_build.log / validate_national.log
├── staging/cbsa_counties.csv       ← the 1,836-county list (statefp,countyfp,geoid,cbsa_name)
├── staging/per_state_counts.txt    ← post-build per-state cell counts by res
└── sql/
    ├── 01b_tiger_blkgrp_national.sql   ← 47-state \copy + insert
    ├── 05_ak_res9.sql                  ← AK antimeridian two-envelope split
    └── 06_validate_national.sql        ← national validation suite (rerunnable)

Comments