A Reddit thread in r/gis titled “ZIP codes are a bad spatial abstraction” accumulated 34 upvotes and substantial agreement. The comments covered reasons ranging from USPS policy to census methodology. The frustration is real: analysts keep getting results that don’t survive peer review, and the problem is usually the spatial unit, not the statistics.

ZIP codes are the default because they appear in every address dataset and everyone understands them intuitively. Neither of those properties makes them good for spatial analysis.

What a ZIP Code Actually Is

The ZIP (Zone Improvement Plan) code was introduced by the USPS in 1963 as a mail routing system. It identifies a delivery unit — a section of a city that one mail carrier or truck can deliver in a day. The geographic boundary of a ZIP code is whatever territory that delivery route covers.

This means:

  • ZIP codes are not stable. The USPS modifies them as mail volume and staffing change. Routes split, merge, and move. There is no guarantee that a ZIP code in your 2020 dataset covers the same area in 2024.
  • Some ZIP codes cover no area. PO Box-only ZIP codes (common in rural areas and business districts) are assigned to a post office building, not a territory. Any polygon dataset for these ZIPs is an interpolation.
  • ZIP codes cross county and state lines. This makes joining to administrative data (property tax records, school enrollment) ambiguous without handling boundary mismatches explicitly.

The ZCTA Problem

The Census Bureau acknowledged this mess and created ZIP Code Tabulation Areas (ZCTAs). ZCTAs are polygons that approximate ZIP code delivery areas based on the most common ZIP code among all Census blocks in a given area. They’re published with the decennial Census and updated between years with TIGER/Line shapefiles.

ZCTAs solve the “no polygon for PO Box ZIPs” problem by collapsing those into adjacent ZCTAs. They also provide stable census geography for demographic joins.

But ZCTAs have their own problems:

  1. They lag real ZIP codes by 2-5 years. ZCTA boundaries are derived from the decennial Census block assignment, which reflects ZIP codes at Census time. Any ZIP code change after that point isn’t reflected until the next update cycle.
  2. ZCTAs and ZIP codes don’t map 1:1. A mailing address with ZIP code 12345 might fall in ZCTA 12345 or might have been absorbed into ZCTA 12346. The Census Bureau provides a ZCTA-to-ZIP crosswalk file, but it has documented mismatches.
  3. They inherit ZIP code size variance. Manhattan ZIP codes cover 0.1 km². Montana rural ZIP codes cover thousands of square kilometers. Any rate or density calculation aggregated to ZCTAs is comparing fundamentally incomparable geographic units.

The Size Problem Is Worse Than It Looks

Manhattan ZIP code 10004 (Financial District) covers approximately 0.5 km². The ZIP code 82401 (Worland, Wyoming) covers approximately 5,900 km². That’s a factor of 11,800 — not an order of magnitude but four.

When you compute a rate per ZIP code — COVID case rates, crime rates, poverty rates — you’re implicitly assuming the geographic units are comparable. A rate of 50 cases per 100k population in Manhattan 10004 represents a dense, walkable neighborhood. The same rate in 82401 represents a sparse rural county the size of a small European country. The spatial relationship between neighboring ZIPs, the concept of “clusters” or “hotspots,” doesn’t transfer across units that vary this much in size.

This shows up clearly when you try to visualize ZIP code data on a map. Large rural ZIPs dominate the visual field regardless of their actual population or economic significance. Chloropleth maps of ZIP code data almost always mislead.

Better Spatial Units for Different Questions

Census Tracts

What they are: Statistical subdivisions of counties, designed to contain approximately 2,500 to 8,000 people (target 4,000). Defined by the Census Bureau with local input, following streets, rivers, and other visible features. Relatively stable over time — they don’t change between decennial Censuses unless the population grows significantly.

When to use them: Demographic analysis, socioeconomic research, health outcomes studies, education analysis. Census tracts are the standard spatial unit for most government and academic research in the US. ACS 5-year estimates are available at tract level for hundreds of demographic variables.

Limitation: Tracts follow county boundaries. Cross-county questions (metro area analysis, regional commute patterns) require assembling and joining multiple county TIGER files.

Getting the data: Census TIGER/Line shapefiles via https://www.census.gov/geographies/mapping-files/time-series/geo/tiger-line-file.html. The file is a shapefile with GEOID field (concatenated FIPS codes: state + county + tract, 11 characters). Load in QGIS or convert with GeoConvert.

Block Groups

What they are: Subdivisions of Census tracts, generally 600 to 3,000 people. A tract contains 1 to 4 block groups typically. Same geometric definition approach as tracts — follow visible features within county boundaries.

When to use them: When tract-level analysis is too coarse for the research question. Neighborhood inequality studies, small-area health analysis, local service area planning.

Limitation: ACS estimates at block group level have larger margins of error than tract-level estimates. For any variable with low counts in the population (rare conditions, small demographic groups), block group estimates are often suppressed or statistically unreliable. Check the margin of error columns before treating block group estimates as reliable.

Census Blocks

What they are: The smallest Census unit — the actual city blocks, bounded by streets, property lines, or other features. Population ranges from 0 (vacant land, commercial zones) to several hundred in dense residential areas.

When to use them: When you need precise spatial allocation — redistributing tract or block group data to parcel-level, building interpolation surfaces, creating custom service areas. Rarely used as the final analysis unit because ACS doesn’t publish estimates at block level (only decennial Census counts).

Limitation: 11 million Census blocks in the US. Loading and joining the full national block dataset in QGIS requires memory management or a PostGIS backend.

H3 Hexagonal Indexing

What it is: Uber’s open-source hierarchical spatial indexing system, released in 2018. Divides the Earth’s surface into hexagons at 16 resolution levels (0 = largest, 15 = smallest). At resolution 8, each hexagon covers approximately 0.74 km² — close to a typical Census tract in a medium-density city. At resolution 9, approximately 0.1 km² — close to a city block.

Why hexagons: Hexagons have equal distance from center to all adjacent cell centers. A square grid doesn’t — diagonal neighbors are farther than cardinal neighbors. For distance-based analysis (service radius, travel time catchments), hexagon grids produce more uniform results than square or polygon administrative grids.

When to use them: Ride-sharing density, delivery zone optimization, telecommunications coverage, any analysis where the underlying phenomenon isn’t administrative but geographic — where you want consistent cell area and neighbor relationships across the study region.

Limitation: H3 doesn’t align with administrative boundaries. Joining H3 cells to Census data requires areal interpolation — distributing the Census block/tract values proportionally based on overlap area. This introduces error. For research requiring Census-derived denominators (per-capita rates), Census units are still the cleaner choice.

Getting started with H3 in Python:

import h3

# Convert a lat/lon to an H3 cell at resolution 8
lat, lon = 40.7580, -73.9855  # Times Square
cell = h3.latlng_to_cell(lat, lon, res=8)
print(cell)  # 882a100d57fffff

# Get all H3 cells covering a bounding box
# (you'll typically use h3.polygon_to_cells for a polygon)
from h3 import h3_to_geo_boundary
boundary = h3.cell_to_boundary(cell)
print(boundary)  # list of (lat, lon) tuples for the hexagon vertices

# Get neighbors within k rings
neighbors = h3.grid_disk(cell, k=2)
print(len(neighbors))  # 19 cells for k=2

H3 cells can be converted to GeoJSON and then processed with QGIS or GeoConvert. The H3 Python library produces FeatureCollection GeoJSON that loads directly into QGIS.

Administrative Boundaries

For questions that are inherently administrative — school enrollment, voting, zoning, tax assessment — use the relevant administrative boundary, not ZIP codes:

  • School districts: Census TIGER SCHOOLDISTRICT files
  • Voting precincts: State GIS portals (not available from Census)
  • County subdivisions (townships, municipalities): TIGER COUSUB
  • Congressional districts: TIGER CD
  • Urban areas: TIGER UA (urbanized areas vs urban clusters)

ZIP codes are sometimes used as a proxy for these administrative boundaries because they’re available in the data. That proxy introduces error. If the actual administrative boundary is available, use it.

Practical QGIS Workflow: Replacing ZIP Codes with Census Tracts

If your data comes with ZIP codes and you need to convert to Census tracts:

Option 1: Spatial join (if you have address points)

  1. Geocode your addresses to lat/lon (QGIS Geocoding or Nominatim batch)
  2. Load Census tract polygons (TIGER shapefile or GeoPackage)
  3. QGIS: Vector > Data Management Tools > Join Attributes by Location
  4. Target layer: your point file; Join layer: Census tracts; Join type: “Take attributes of the first located feature only”
  5. The output feature has both your original attributes and the Census GEOID appended

Option 2: ZCTA-to-tract crosswalk (if you only have ZIP codes, no addresses)

The Census Bureau publishes a ZCTA-to-tract relationship file. It lists every ZCTA/tract pair that overlap, with the percentage of the ZCTA that falls within the tract (by land area). This allows proportional allocation:

  1. Download the relationship file from data.census.gov > ZCTA-to-tract crosswalk
  2. Join your ZCTA-level data to the crosswalk by ZCTA code
  3. Multiply each attribute value by the overlap percentage to distribute it proportionally across tracts
  4. Sum by tract GEOID

This is an approximation — it assumes uniform distribution within ZCTAs — but it’s far more defensible than treating ZIP codes as equivalent units.

The Format Conversion Trap

When working with Census geographic data, the standard download is a shapefile (TIGER/Line). If you’re combining Census tract data with GeoJSON sources — common when pulling from APIs or web data sources — you’ll run into field name truncation.

The Census GEOID field (which identifies tracts, block groups, etc.) looks like: "GEOID": "36061023100" in GeoJSON. When converted to shapefile, GEOID (5 characters) is preserved without truncation. But longer field names are not:

GeoJSON field name      → Shapefile field name (10-char max)
"TOTAL_POPULATION"      → "TOTAL_POPU"  (truncated)
"MEDIAN_HOUSEHOLD_INC"  → "MEDIAN_HOU"  (truncated)
"PERCENT_BELOW_POVERTY" → "PERCENT_BE"  (truncated)

If you’re joining shapefile Census data to another dataset using a field like TOTAL_POPU (not knowing it was truncated from TOTAL_POPULATION), the join will silently fail if the other dataset still uses the full name. Always verify field names after converting with any tool. See why GeoJSON-to-Shapefile conversions silently corrupt your data for the full list of silent truncation and type coercion issues.

GeoConvert preserves field semantics and reports truncations explicitly in its output log, so you know immediately which fields were modified. For Census data specifically, the GEOID fields are all 11 characters or fewer — they come through intact. The long human-readable field names from ACS downloads are the ones that get hit.

When ZIP Codes Are Actually the Right Choice

ZIP codes are appropriate when the analysis is inherently about mail delivery or when you have no choice because the data only exists at ZIP code level:

  • Healthcare insurance plan networks (defined by ZIP code service areas)
  • Shipping cost zones (carriers define these by ZIP code)
  • Sales tax nexus analysis (some state tax codes use ZIP codes)
  • Marketing list analysis where the underlying data is ZIP-coded and no address-level data exists

In these cases, use ZCTAs (not the raw ZIP code polygons from commercial providers, which are often outdated approximations) and document the limitations clearly. For everything else — public health, demographic research, economic analysis, environmental justice — get off ZIP codes and onto Census geography.

The spatial unit you choose shapes what you can see. ZIP codes were designed to optimize mail delivery, and they’re very good at that. They weren’t designed to support spatial inference, and using them for that produces results that look plausible until they don’t.

For getting Census TIGER shapefiles into formats compatible with your analysis pipeline — QGIS, ArcGIS, or custom tools — see the GIS file conversion silent failures to check before trusting your output. Census shapefiles are generally clean, but the attribute table encoding and field type mapping can vary by year and TIGER vintage.