Choosing a Projected CRS for a Municipal Compliance Project
The working coordinate frame is chosen once at the start of a compliance project and lived with for years, because changing it later invalidates every historical comparison. This guide walks through the decision as a sequence of eliminations — coverage, distortion, unit, authority match — ending with a recorded configuration entry that every later stage asserts against. It applies the principles from CRS standardization and datum management to the specific moment a project starts.
Prerequisites
Step-by-step
Step 1: Eliminate the frames that cannot measure
Two candidates disqualify themselves immediately and both appear in real projects. A geographic frame measures in degrees, so every distance operation returns an angle; Web Mercator measures in metres and is wrong by the secant of the latitude, which at 38° north is about 27%. The first failure is obvious, the second is not, which makes Web Mercator the more dangerous of the two.
from pyproj import CRS
for code in (4326, 3857, 2227, 26943):
crs = CRS.from_epsg(code)
axis = crs.axis_info[0]
print(f"EPSG:{code:<6} projected={crs.is_projected!s:<5} unit={axis.unit_name}")
Any candidate whose is_projected is false is out. Web Mercator passes that test and still fails the project, which is why the elimination has to be by name rather than by predicate alone.
Step 2: Find the zones that cover the study area
State Plane and UTM both divide the country into zones, and a project should sit inside one. Query the candidates by area of use rather than by memory, since counties near a zone edge are easy to misassign.
from pyproj.aoi import AreaOfInterest
from pyproj.database import query_crs_info
# Bounding box of the study area, in longitude/latitude order.
aoi = AreaOfInterest(west_lon_degree=-118.7, south_lat_degree=33.7,
east_lon_degree=-117.6, north_lat_degree=34.4)
candidates = query_crs_info(auth_name="EPSG", area_of_interest=aoi,
pj_types="PROJECTED_CRS")
for c in candidates[:12]:
print(f"EPSG:{c.code:<7} {c.name}")
If two zones both cover part of the area, the project spans a zone boundary and has a decision to make: split processing by zone and reconcile at the seam, or define a single project projection for the whole area and record its full definition. Stretching one zone across the boundary is the option to avoid, since distortion grows with distance from the central meridian and is worst exactly where two jurisdictions meet.
Step 3: Match the authority you will be compared against
Where more than one zone or frame covers the area, the tie-breaker is whose numbers your results will be checked against. County assessors publish acreages computed in their own State Plane zone; if the pipeline reports areas from a UTM frame, small differences appear in every reconciliation and somebody has to explain them each time.
Ask for the county’s published frame directly, or read it from their parcel download. Matching it removes an entire recurring category of argument, and the residual distortion inside a State Plane zone is designed to stay under roughly one part in ten thousand — well below anything a compliance rule notices.
Step 4: Pick the linear unit from the ordinance
Zone and unit are two separate decisions and the unit is the one that causes damage when it is left implicit. Many State Plane zones are published under two EPSG codes — one in metres, one in US survey feet — describing the same zone. Choose the one the code speaks, so a “20-foot rear setback” is compared against a number already in feet with no conversion in between.
FOOT_IN_M = 1200 / 3937 # US survey foot, exactly
def unit_of(epsg: int) -> str:
return CRS.from_epsg(epsg).axis_info[0].unit_name
assert unit_of(2227) == "US survey foot" # California zone 3, feet
assert unit_of(26943) == "metre" # California zone 3, metres
Where the pipeline serves both a foot-based ordinance and metric national datasets, pick the ordinance’s unit for the working frame and convert on the way in, not on the way out. Conversions at the rule boundary are where factor-of-3.28 errors hide.
Step 5: Record the decision where the pipeline reads it
The last step is the one that makes the previous four durable. Write the EPSG code, the unit, the reason and the date into project configuration, and have every stage assert against it rather than assuming.
WORKING_FRAME = {
"epsg": 2227,
"unit": "US survey foot",
"chosen_because": "matches the county assessor's published parcel frame; "
"ordinance § 17 expresses setbacks in feet",
"decided_on": "2026-08-08",
}
def assert_frame(gdf, layer: str):
crs = gdf.crs
if crs is None or crs.to_epsg() != WORKING_FRAME["epsg"]:
raise ValueError(f"{layer}: expected EPSG:{WORKING_FRAME['epsg']}, got {crs}")
if crs.axis_info[0].unit_name != WORKING_FRAME["unit"]:
raise ValueError(f"{layer}: unit {crs.axis_info[0].unit_name}")
return gdf
Verification
Confirm the choice against a known quantity before building anything on it. Reproject a sample of parcels into the chosen frame, compute their areas, and compare against the assessor’s published acreage. Agreement to within a few thousandths of an acre confirms zone and unit together; a systematic ratio points at the unit, and a scatter that grows with distance from the zone centre points at the wrong zone.
parcels = gpd.read_file("parcels.gpkg").to_crs(f"EPSG:{WORKING_FRAME['epsg']}")
parcels["acres"] = parcels.geometry.area / 43560.0 # sq ft per acre
delta = (parcels["acres"] - parcels["assessor_acres"]).abs()
print(f"median |delta| = {delta.median():.4f} ac, max = {delta.max():.4f} ac")
assert delta.median() < 0.005, "zone or unit mismatch — do not proceed"
Common Pitfalls
- Choosing the frame the basemap uses. The display frame and the analysis frame are different decisions. Reproject for display as the last step and never measure in the display frame.
- Assuming the metre variant because the code is lower. EPSG numbering carries no meaning here. Read
axis_info[0].unit_nameon the actual code you intend to use. - Deciding by team preference rather than by the authority. An organisation-wide frame is convenient for engineers and produces areas that disagree with every county record the team works against.
- Recording the choice in a README. A frame that is documented but not asserted drifts the first time a new source is added by somebody who was not in the room.
Frequently Asked Questions
What if the study area genuinely spans two zones?
Split processing by zone and reconcile at the seam, or define a single project projection — a Lambert or transverse Mercator centred on the area — and record its complete PROJ definition rather than an EPSG code. Both are defensible; stretching one published zone across the boundary is not.
Can the frame be changed later?
Yes, but treat it as a breaking change with a migration. Every area and distance will shift slightly, and those shifts must be attributed to the frame change rather than mistaken for rule drift — which requires a regression run over historical parcels, as described in compliance testing and regression suites.
Does the datum realisation matter for this decision?
Less than the zone and the unit, but it is not nothing: NAD83(2011) and the 1986 realisation differ by centimetres to decimetres. Pick the realisation the authoritative data uses, record it, and handle any shift explicitly as in handling datum shifts from NAD83 to WGS84.
Should every jurisdiction in a multi-county deployment share one frame?
No. Choose per jurisdiction to match the authority whose numbers you will be compared against, and keep the frame as configuration rather than as a constant. A shared frame is convenient exactly once, at the start, and inconvenient at every reconciliation afterwards.
How long should this decision take?
An afternoon, once, at the start of a project — and it is worth protecting that afternoon. The alternative is not a faster decision but an implicit one, made by whoever wrote the first script, discovered months later when somebody notices the areas do not match the assessor’s. Every step above is a lookup or a query rather than a judgement, and the only genuinely deliberative part is step three, which usually resolves with one phone call to the county GIS office.
Record the answer wherever the pipeline will read it, and the decision is made for good rather than made again by each new contributor.
Related
Part of: CRS standardization and datum management
- Validating CRS metadata before a compliance run — the pre-flight gate that enforces this decision.
- Reprojecting parcel layers to State Plane in GeoPandas — applying the chosen frame to parcel fabric.
- Best practices for CRS standardization in compliance GIS — the standing rules around this choice.
- Unit conversion pitfalls in setback thresholds — where the unit decision is felt downstream.