Block-Group ↔ H3 Crosswalk + hex_hta
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 incrosswalk/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
load_tiger_blkgrp.py(geopandas/pyogrio in the gpu-pilot venv) → CSV with NAD83 WKB hex (5.8 s for all four states).sql/01_tiger_blkgrp.sql— temp-stage\copy, thenST_Multi(ST_Transform(ST_SetSRID(geom,4269),4326))intotiger_blkgrp(~7 s).sql/02_crosswalk.sql—h3_polygon_to_cells(geom, res)from h3-pg 4.2.3'sh3_postgisextension. 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 NOTHINGabsorbs degenerate boundary ties). res 9 ≈ 0.105 km²/cell, res 10 ≈ 0.015 km²/cell.sql/03_hex_hta.sql— plain view;hta_indexjoin keys (geoid, year=2022, level='blkgrp').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_demographicsnyc (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 throughhex_hta.- Raw
nyc_tristate_h3_res9.parquetgrid (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:
curl https://www2.census.gov/geo/tiger/TIGER2022/BG/tl_2022_{fips}_bg.zipfor 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.load_tiger_blkgrp.py <fips> …→ CSV → same01SQL (append new\copylines or loop).- 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). hex_htaneeds no change —hta_indexis 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)