Choosing a CRS for Multi-Jurisdiction Water Networks
A regional water authority rarely respects the tidy boundaries of a projected coordinate system. A single pressure zone can straddle two State Plane zones; an interconnection main can run from one UTM zone across the 6-degree meridian into the next; a wholesale supplier can deliver into three counties that were each digitized in a different local frame. The moment those datasets are asked to behave as one connected network, the choice of a coordinate reference system stops being a cartographic default and becomes a load-bearing engineering decision. Pick a frame that distorts length near the edge of the service territory and the network’s connectivity tolerance silently absorbs the error until two coincident points fail to snap; pick a frame that changes at a zone seam and every trace that crosses the seam either stops dead or reports a wrong isolation boundary. This decision belongs to the discipline of CRS alignment and geodetic transformations, and it inherits the accuracy expectations set out in the broader core utility GIS fundamentals reference. The task here is narrower and sharper: given a water network that spans jurisdictions, how do you choose the one authoritative CRS it will live in?
The answer is not a rule of thumb about which zone a city falls in. It is a scored comparison between three families of projection — State Plane, UTM, and a custom Lambert conformal conic fitted to the actual extent — measured against the network’s own tolerance budget. This page supplies that decision framework, a runnable helper that quantifies distortion for each candidate, and a deployment pattern that makes the chosen frame enforceable rather than aspirational.
Environment Prerequisites
Settle these before scoring a single projection; a distortion comparison run on undefined or mixed-datum inputs measures noise, not the projections.
- Software: ArcGIS Pro 3.2+ (Standard or Advanced) for the authoritative geodatabase, plus Python 3.11 with
pyproj>=3.4,geopandas>=1.0, andshapely>=2.0in an isolatedcondaenvironment created withconda create -n un-crs python=3.11. - PROJ grids: Confirm
PROJ_DATAresolves to a directory holding the NADCON5/NTv2 grid-shift files. Without them, a NAD27-to-NAD83 or NAD83(2011) realization transform falls back to a coarse three-parameter shift that reintroduces a half-metre of drift — larger than the connectivity tolerance you are trying to protect. Verify withpython -c "import pyproj; print(pyproj.datadir.get_data_dir())". - A single geographic datum decision, made first. Choose the datum (for example NAD83(2011)) before the projection. Mixing NAD83(2011) and WGS84 sources without an explicit transformation introduces a systematic offset of roughly a metre in the continental US that no projection choice can correct.
- The full asset extent as geographic coordinates. Export every feature class the network will contain — mains, valves, hydrants, service points — reprojected to EPSG:4269 (NAD83 geographic) so the extent envelope is measured in one lat/long frame regardless of each source’s original projection.
- The network connectivity tolerance from the design standard. You cannot score distortion without the budget it is measured against; typical utility network XY tolerance sits in the 0.001–0.01 m range, and the linear distortion of the projection must stay far below the survey tolerance (commonly 0.1–0.5 m for sub-meter mapping).
Schema-Aware Extent Protocol — Run Before You Score
Most CRS mistakes are made before any projection is compared, by scoring the wrong extent or comparing across an undeclared datum. Work this ordered checklist first; the earliest item is the most frequent cause of a confidently wrong choice.
- Confirm every source declares a CRS, and compare by EPSG code, not by name. A layer with
crs is Nonecarries no georeference and must be identified and defined before it contaminates the extent envelope. Two layers both labeled “State Plane” can be different zones or different datum realizations; onlygdf.crs.to_epsg()is authoritative. - Separate the datum question from the projection question. Distortion analysis assumes all candidates share one geographic datum. If any source is on NAD27 or an older NAD83 realization, reproject it with a grid-based transformation first, then measure projections. A datum mismatch masquerades as projection distortion and derails the whole comparison.
- Measure the true operational envelope, including planned interconnections. Score against the extent the network will have, not the extent it has today. A wholesale interconnection two counties away can push the envelope across a UTM seam that the current data never approaches, invalidating a UTM choice that looked fine at digitization time.
- Identify every zone seam the extent crosses. List the State Plane zones and UTM zones the envelope intersects. Any candidate whose zone boundary falls inside the operational extent is disqualified as a single authoritative frame — a network cannot be continuous across a projection discontinuity.
- Fix the tolerance budget as a hard number. Write down the connectivity tolerance and the survey tolerance as explicit values. The scoring step rejects candidates by comparing measured distortion against these numbers; leaving them implicit turns an objective test into a preference.
Minimal Reproducible Implementation
The centerpiece of this decision is a scored comparison. The table below evaluates the three projection families against the four criteria that decide correctness for a spanning water network. Treat it as the decision matrix: read down the column that matches your extent, and disqualify any row that fails the tolerance budget outright.
| Criterion | State Plane (per-zone) | UTM (per-zone) | Custom Lambert Conformal Conic |
|---|---|---|---|
| Linear distortion | Very low inside a zone (~1:10,000 by design); grows sharply beyond the zone’s designed width | Moderate; up to ~1:2,500 (≈0.4 m/km) near the 3-degree zone edge | Tunable — standard parallels fitted to the extent hold distortion to ~1:20,000 or better across the whole territory |
| Zone-crossing behavior | Discontinuous — a network spanning two zones has two frames; coordinates jump at the seam | Discontinuous — 6-degree zones; a seam inside the extent breaks continuity | Continuous — one frame covers the entire multi-jurisdiction extent with no seam |
| Connectivity tolerance fit | Excellent within one zone; fails where the extent leaves the zone and distortion exceeds the XY tolerance | Marginal at zone edges; distortion can approach the survey tolerance on a wide territory | Excellent everywhere by construction, because the parallels are placed to bound worst-case distortion |
| Interoperability | Highest — regulators, counties, and CAD deliverables expect State Plane; well-understood EPSG codes | High — universal, metric, favored for regional and cross-agency exchange | Lowest — a non-standard definition every consumer must be handed explicitly; needs a documented WKT/EPSG-style definition |
| Best fit | Network wholly inside one State Plane zone, heavy county/CAD exchange | Network inside one UTM zone, or a metric regional frame with modest width | Network that genuinely spans two or more zones and must trace continuously across them |
The matrix narrows the field to at most two viable candidates; the helper below decides between them on measured evidence rather than nominal zone limits. It projects a set of sample segments spanning the extent into each candidate CRS, compares each projected length against the geodesic (“true”) length on the ellipsoid, and reports the worst-case linear distortion so it can be checked against the tolerance budget.
import geopandas as gpd
import pandas as pd
from pyproj import Geod, Transformer
from shapely.geometry import LineString
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
_GEOD = Geod(ellps="GRS80") # NAD83 reference ellipsoid
def score_crs_candidates(
extent_lines: gpd.GeoDataFrame,
candidates: dict[str, str],
tolerance_m: float = 0.10,
) -> pd.DataFrame:
"""Rank candidate projected CRSs by measured linear distortion.
``extent_lines`` holds sample segments (in geographic NAD83, EPSG:4269)
that span the operational extent. ``candidates`` maps a label to an EPSG
string, e.g. {"UTM17N": "EPSG:26917"}. For each candidate the projected
segment length is compared to the geodesic length; the worst-case relative
error is returned so it can be judged against the connectivity/survey budget.
"""
if extent_lines.crs is None or extent_lines.crs.to_epsg() != 4269:
raise ValueError("extent_lines must be defined in EPSG:4269 (NAD83 geographic)")
rows = []
for label, epsg in candidates.items():
transformer = Transformer.from_crs("EPSG:4269", epsg, always_xy=True)
worst_rel = 0.0
for geom in extent_lines.geometry:
if geom is None or geom.is_empty:
continue
lons, lats = zip(*list(geom.coords))
geodesic_m = _GEOD.line_length(lons, lats) # true length on ellipsoid
xs, ys = transformer.transform(lons, lats)
projected_m = LineString(zip(xs, ys)).length
if geodesic_m > 0:
rel = abs(projected_m - geodesic_m) / geodesic_m
worst_rel = max(worst_rel, rel)
# distortion expressed over a nominal 1 km connection
worst_m_per_km = worst_rel * 1000.0
rows.append(
{
"candidate": label,
"epsg": epsg,
"worst_ratio": f"1:{int(1 / worst_rel):,}" if worst_rel else "1:inf",
"worst_m_per_km": round(worst_m_per_km, 4),
"within_budget": worst_m_per_km <= tolerance_m,
}
)
logging.info("%s: worst-case %.4f m/km (%s)", label, worst_m_per_km,
"OK" if worst_m_per_km <= tolerance_m else "REJECT")
result = pd.DataFrame(rows).sort_values("worst_m_per_km").reset_index(drop=True)
return result
if __name__ == "__main__":
# Two sample mains spanning a territory near the SPCS/UTM seam.
lines = gpd.GeoDataFrame(
{"name": ["west_main", "east_interconnect"]},
geometry=[
LineString([(-81.9, 28.0), (-81.2, 28.4)]),
LineString([(-81.2, 28.4), (-80.6, 28.9)]),
],
crs="EPSG:4269",
)
candidates = {
"SPCS_FL_E": "EPSG:2236", # State Plane Florida East (ftUS)
"UTM17N": "EPSG:26917", # UTM zone 17N
"Custom_LCC": "EPSG:5070", # CONUS Albers stand-in for a fitted LCC
}
print(score_crs_candidates(lines, candidates).to_string(index=False))
The helper turns the matrix’s qualitative “very low / moderate / tunable” into a number you can defend in a design review: a candidate that reports more than a small fraction of the connectivity tolerance across the extent is disqualified no matter how convenient its EPSG code. The following diagram condenses the whole decision into the sequence of gates it actually is.
When the two candidates score close to each other, break the tie on interoperability, not on marginal distortion: if the whole extent fits one State Plane zone within budget, prefer it for the county and CAD exchange it enables; only reach for a custom Lambert when the extent genuinely crosses a seam and no standard zone holds the tolerance. Whatever you pick, record the worst-case distortion number alongside the choice — it is the evidence that the frame is fit for purpose, and it aligns the decision with the sub-meter mapping precision standards the rest of the network is held to.
Production Deployment Pattern
A CRS chosen in a notebook and forgotten is worse than no decision, because every later import quietly reintroduces the frames you rejected. Make the choice enforceable:
- Publish one authoritative CRS as configuration. Store the selected EPSG code (or the full WKT for a custom Lambert) in a version-controlled settings file that both the geodatabase and every ingestion script read. No script hard-codes a projection; they all resolve it from the single source of truth.
- Gate ingestion on the frame. Every source feed passes through a reprojection-and-verification step before it reaches the network, exactly as the data ingestion pipelines for utility assets prescribe. A feed that arrives in an unexpected datum is reprojected with an explicit grid-based transformation and logged; a feed with no declared CRS fails the build rather than being silently assumed.
- Pin the transformation path per source. Record the exact transformation used to bring each jurisdiction’s data into the authoritative frame — NAD27→NAD83(2011) via NADCON5, for example — so the pipeline is reproducible and a PROJ upgrade cannot silently change the result. Validate CRS conformance in continuous integration with the companion Python script for validating CRS alignment across utility layers.
- Re-score when the extent grows. A new interconnection or an annexed service area can push the envelope across a seam that the original choice never touched. Re-run the distortion scoring whenever the operational extent changes materially, and treat a newly-failing budget as a design change, not a rounding error.
- Carry the frame into tracing. The chosen CRS is what keeps a trace continuous across a jurisdictional boundary; a sub-tolerance gap introduced by the wrong projection is indistinguishable from a real disconnect to the topology and tracing workflows that consume the network. Document the frame in the network metadata so every downstream trace inherits it.
Conclusion
Choosing a coordinate reference system for a water network that spans jurisdictions is a measured trade-off, not a lookup. State Plane wins on interoperability and holds sub-tolerance distortion inside its designed zone; UTM offers a clean metric frame until the extent crosses a 6-degree seam; a custom Lambert conformal conic buys continuity across the whole territory at the cost of being a non-standard definition every consumer must be handed. The decision matrix narrows the field, the distortion helper decides between the survivors on evidence, and the tolerance budget is the arbiter that turns preference into engineering. Commit one authoritative frame, pin every transformation path, and re-score when the network grows — and the CRS stops being a source of silent connectivity failures and becomes the stable ground the rest of the automation stands on.
Related
- Up to the parent topic: CRS Alignment & Geodetic Transformations
- Up to the section: Core Utility GIS Fundamentals & Network Models
- Python Script for Validating CRS Alignment Across Utility Layers
- Precision Standards for Sub-Meter Mapping
- Data Ingestion Pipelines for Utility Assets
For authoritative reference, consult the pyproj documentation and the OGC coordinate reference systems standards.