Extracting Transmission Corridor Widths from Line Geometry
The scenario: a co-location study needs to know how much right-of-way each transmission corridor occupies, the source dataset carries centrelines and nothing else, and the analyst applies a flat 30-metre buffer to everything. The 500 kV double-circuit corridor is under-stated by a factor of four, the 69 kV tap is over-stated, and the resulting land-take figure is wrong in both directions at once. This page infers width from what the geometry actually shows, and it extends transmission line and substation mapping.
Root-cause analysis
Centreline datasets omit width, and three pieces of evidence in the data can stand in for it.
- Voltage class. Right-of-way width scales with voltage because clearance does: a 69 kV single circuit typically occupies 20 to 30 metres, a 230 kV circuit 40 to 55, and a 500 kV circuit 60 to 90. These are conventions rather than laws, and they vary by utility, but they bound the answer.
- Parallel runs. Two or more circuits mapped as separate ways following the same corridor within tens of metres are almost always sharing one right-of-way. Buffering each independently and unioning double-counts the shared land; measuring the envelope of the group does not.
- Structure spacing. Where towers are mapped, the span length is evidence about the structure type, and structure type correlates with width more tightly than voltage alone — a 230 kV line on monopoles occupies materially less than the same voltage on lattice towers.
Pre-flight validation
Find the parallel runs first, because they change which lines should be measured together.
import geopandas as gpd
import numpy as np
def find_parallel_runs(
lines: gpd.GeoDataFrame,
*,
working_epsg: int,
max_separation_m: float = 120.0,
min_shared_length_m: float = 500.0,
) -> list[list]:
"""Group circuits that share a corridor, so their widths are not double-counted."""
proj = lines.to_crs(working_epsg)
idx = proj.sindex
groups: list[set] = []
for i, geom in zip(proj.index, proj.geometry):
candidates = list(idx.query(geom.buffer(max_separation_m), predicate="intersects"))
near = set()
for pos in candidates:
j = proj.index[pos]
if j == i:
continue
other = proj.geometry.loc[j]
# Shared length: how much of each line lies within the other's corridor buffer.
shared = geom.intersection(other.buffer(max_separation_m)).length
if shared >= min_shared_length_m:
near.add(j)
if near:
near.add(i)
merged = False
for g in groups:
if g & near:
g |= near
merged = True
break
if not merged:
groups.append(near)
return [sorted(g) for g in groups]
A corridor with four mapped circuits and no grouping produces four buffers and four times the land take; grouping first turns that into one envelope, which is what exists on the ground.
Fix implementation
import geopandas as gpd
# Half-widths in metres by voltage class, single circuit on lattice towers.
ROW_HALF_WIDTH_M = {69: 12.5, 115: 17.5, 138: 20.0, 230: 25.0, 345: 32.5, 500: 42.5, 765: 55.0}
MONOPOLE_FACTOR = 0.7 # monopoles need materially less lateral clearance
PER_EXTRA_CIRCUIT_M = 12.0 # each additional parallel circuit widens the envelope
def estimate_corridor_widths(
lines: gpd.GeoDataFrame,
*,
working_epsg: int,
groups: list[list] | None = None,
voltage_field: str = "voltage_kv",
structure_field: str = "structure",
) -> gpd.GeoDataFrame:
"""Per-corridor right-of-way estimate, with the evidence and a confidence label."""
proj = lines.to_crs(working_epsg)
groups = groups or [[i] for i in proj.index]
rows = []
for members in groups:
subset = proj.loc[members]
voltages = subset[voltage_field].dropna()
if voltages.empty:
half, confidence, basis = 20.0, "low", "no voltage — default applied"
else:
nominal = min(ROW_HALF_WIDTH_M, key=lambda v: abs(v - voltages.max()))
half = ROW_HALF_WIDTH_M[nominal]
basis = f"voltage class {nominal} kV"
confidence = "medium"
structures = subset.get(structure_field)
if structures is not None and (structures == "monopole").all():
half *= MONOPOLE_FACTOR
basis += " · monopole"
confidence = "high"
extra = max(0, len(members) - 1)
half += extra * PER_EXTRA_CIRCUIT_M
if extra:
basis += f" · {extra} parallel circuit(s)"
envelope = subset.geometry.union_all().buffer(half)
rows.append({
"geometry": envelope,
"members": list(members),
"circuits": len(members),
"half_width_m": half,
"row_width_m": half * 2,
"basis": basis,
"confidence": confidence,
"length_km": float(subset.geometry.length.sum()) / 1000.0,
"area_ha": envelope.area / 10_000.0,
})
return gpd.GeoDataFrame(rows, crs=proj.crs)
The basis and confidence fields are the point. A width inferred from a voltage class alone is a
different claim from one corroborated by structure type, and a land-take figure built from the two
should say which it rests on.
Fallback routing and performance tuning
- Group before buffering, always. Buffering each circuit and unioning is both slower and wrong; the union of four 25-metre buffers over a shared corridor is not the corridor.
- Cap the grouping distance by voltage. A 120-metre separation is reasonable for bulk transmission and far too generous for distribution, where two circuits 100 metres apart are genuinely separate corridors.
- Use the envelope, not the convex hull. A convex hull across a bend swallows land the corridor does not occupy; buffering the unioned centrelines follows the alignment.
- Simplify centrelines before buffering. A one-metre simplification on a national line layer cuts the buffer cost substantially and moves the envelope edge by less than the width uncertainty.
- Treat width as a range, not a number. Publishing a low and high estimate alongside the central one is more useful than a single figure with false precision.
Downstream validation
def assert_corridor_widths(corridors, lines, *, max_width_m: float = 200.0) -> None:
"""Bounds and coverage checks on an inferred right-of-way layer."""
assert corridors["row_width_m"].between(20, max_width_m).all(), (
"an inferred corridor width falls outside any plausible right-of-way range"
)
assigned = {m for members in corridors["members"] for m in members}
missing = set(lines.index) - assigned
assert not missing, f"{len(missing)} circuits were not assigned to any corridor"
assert corridors["confidence"].isin({"low", "medium", "high"}).all(), "unlabelled confidence"
# A grouped corridor must never be narrower than a single circuit of the same class.
singles = corridors[corridors["circuits"] == 1]["row_width_m"].max()
grouped = corridors[corridors["circuits"] > 1]["row_width_m"].min()
if len(corridors[corridors["circuits"] > 1]):
assert grouped >= singles * 0.9, "a multi-circuit corridor came out narrower than a single one"
What the width estimate is good for, and what it is not
An inferred right-of-way is a screening quantity, and being explicit about that prevents most of the misuse.
It is good for land-take estimation across a region: how many hectares existing corridors occupy, how much of a study area is already encumbered, and how a proposed route compares with existing infrastructure. Errors of ten or twenty percent on individual corridors average out across hundreds of kilometres.
It is good for co-location screening: identifying where a new line could plausibly share an existing corridor, which is one of the highest-value weights in a least-cost routing surface. The question there is whether a corridor exists and roughly how wide, not its legal extent.
It is not good for anything that touches a property boundary. The legal right-of-way is defined by recorded easements, not by clearance conventions, and it frequently differs from the inferred figure by tens of metres in either direction. A parcel-level encumbrance question needs the easement record, and an inferred width used in its place will be wrong on exactly the parcels where it matters.
It is also not a substitute for a survey where the question is constructability. Two corridors of the same nominal width can differ entirely in usable space depending on terrain, access and existing crossings — which is why the routing surface treats an existing corridor as a cost discount rather than as a guaranteed alignment.
Frequently asked questions
Where do the half-width conventions come from?
Utility design standards and published transmission planning documents, which broadly agree at each voltage class and differ in the details. Because they are conventions, they belong in configuration with a citation, and a utility whose standards are known should override the defaults for its own territory.
How do I detect double-circuit lines from geometry alone?
You largely cannot — a double-circuit tower carries two circuits on one structure and appears as one
way. The circuits tag is the evidence when present, and its absence is why the confidence label
matters. Parallel-run detection finds circuits on separate structures, which is a different and
easier case.
Should the corridor include the access road?
For land-take estimation, yes, and the conventions above generally already reflect maintained access within the right-of-way. For a constructability question the access route may run well outside the corridor, and that is a routing problem rather than a width one.
What about underground cable?
It has a right-of-way too, usually much narrower — often 6 to 15 metres — and no overhead clearance requirement. Because the cable tag is recorded on the way, the estimator should branch on it rather than applying overhead conventions, which over-state underground land take by a factor of three or more.
How should low-confidence estimates be presented?
As a range with the basis named, and excluded from any total that will be quoted without qualification. A corridor whose width rests on a defaulted voltage should not be silently summed with one corroborated by structure type — reporting the two subtotals separately keeps the aggregate honest.
Can the estimate be validated against imagery?
Yes, and it is the most practical check available. Measuring the cleared corridor on recent imagery for a sample of twenty corridors per voltage class calibrates the conventions for a specific territory in an afternoon, and the calibrated table then applies to the whole region.
Should widths be published as a layer or as attributes on the centrelines?
Both, and they answer different questions. The polygon layer is what a land-take or co-location query needs; the width attribute on the centreline is what a routing surface needs, because a cost raster wants a number per cell rather than a polygon to intersect. Deriving the polygon from the attribute keeps the two consistent, and storing only the polygon makes the routing case rebuild it every run.
How often should the estimate be refreshed?
Whenever the source circuits change, which for an OpenStreetMap-derived layer is continuously and for a utility extract is quarterly at most. The estimate is cheap to recompute once the parallel-run grouping is cached, so the practical cadence follows the source rather than a schedule of its own.
Related
- Transmission Line & Substation Mapping — the parent workflow and its tag semantics
- Mapping High-Voltage Transmission Lines from OpenStreetMap — the tag completeness this inference works around
- Deduplicating Overlapping Transmission Segments from OpenStreetMap — separating duplicates from genuine parallel runs
- Building a Transmission Cost Surface Raster in NumPy — where the corridor discount is applied