What export_elevation.py does to put ATC's centerline in trail order, why it
is wrong in a measurable way, what the source geometry actually looks like, and
which fixes have been tried and rejected.
This is a record, not a plan. The ordering is a known-open problem (#652 — The elevation profile's mile axis is out of order in 18 places, by up to 46 miles) and is deliberately not being worked. It is written down because the measurements below cost a couple of hours and would otherwise have to be re-derived from issue comments by whoever picks it up.
It is also not a blocker for the gain figures any more. #559 is fixed by
marking the seams rather than by ordering them correctly — see
export_elevation.py's docstring point 4 — and that fix stays correct however
good the ordering ever gets. What ordering still affects is distance_mi: a
sample's mile is its position in the sorted sequence, so where the sort is
wrong, the mile axis is wrong.
ordered_oriented_parts() reverses any piece whose own coordinates run
north-to-south, then sorts every piece by its start point's
_trail_axis_projection — the projection onto a straight Springer→Katahdin
line. The function's docstring is upfront that this is a geographic
approximation rather than a reconstruction, since centerline.geojson carries
no trail-sequence field.
Against the live ATC centerline, 2026-08-13:
pieces after ST_LineMerge |
558 |
| their total length | 2,162.6 mi |
| junctions between them | 557 |
| median junction gap | 0 ft |
| junctions over 0.1 mi | 143 |
| junctions over 10 mi | 18 |
| total gap | 674.7 mi |
| largest single jump | 46.5 mi |
Two of those settle what kind of problem it is. The median gap is exactly zero — 414 of 557 junctions join perfectly — and the pieces already add up to the whole trail, 2,162.6 mi against roughly 2,197. So a 46.5-mile "gap" is not missing trail. It is the sort leaping 46 miles away and coming back.
Union-find over piece endpoints coinciding within 1 m:
558 pieces -> 6 connected components
774.4 mi (175 pieces)
680.1 mi (139 pieces)
501.0 mi (133 pieces)
105.3 mi ( 64 pieces)
101.8 mi ( 46 pieces)
0.0 mi ( 1 piece — degenerate, zero length)
ATC's centerline has five genuine discontinuities, not 557. The 143 bad junctions are almost entirely the straight-line sort interleaving pieces from the 774-mile chain with pieces from the 680-mile chain and back again.
Which sounds like the fix is easy, and then the node degrees say otherwise — counting how many piece-ends meet at each point:
degree 1 -> 15 nodes (chain ends)
degree 2 -> 403 nodes (ordinary joins)
degree 4 -> 1 node
degree 5 -> 3 nodes
degree 6 -> 44 nodes
degree 12 -> 1 node
Forty-eight nodes have four or more piece-ends coinciding; one has twelve. A trail does not branch six ways. That is duplicate or overlapping geometry surviving the merge — 3,025 raw features collapsing to 558 pieces leaves room for it — and it means a component cannot be walked as a chain until something decides what happens at those nodes.
| approach | total gap | largest jump | junctions > 0.1 mi |
|---|---|---|---|
| current straight-axis sort | 674.7 mi | 46.5 mi | 143 |
| greedy nearest-endpoint chain | 1,133.4 mi | 474.3 mi | 24 |
| component walk from a degree-1 end | 2,176.2 mi | 473.8 mi | — |
Greedy nearest-endpoint connects 534 of 557 junctions essentially perfectly — a large improvement on the count — and then paints itself into a corner and leaps 474 miles back to pick up what it skipped. That is the classic greedy failure and it makes the total distance worse than what we have.
Walking each component from a degree-1 end does worse still: the walk wanders at the degree-6 nodes, leaves most of the component unreached, and the unreached pieces get appended arbitrarily.
Neither was a serious attempt and neither should be read as proving the problem unsolvable. They are recorded because both look obviously correct before you run them.
In this order, none of it verified:
- Dedupe the overlapping geometry first. The degree-6 nodes are the blocker, and they are upstream of any walking strategy. Worth finding out whether they are exact duplicate segments, near-duplicates from separate surveys, or genuine multi-way junctions that mean something.
- Walk each component once its nodes are mostly degree ≤ 2, starting from a degree-1 end. Five components, so five walks.
- Order the five chains geographically — this is the only step the current straight-axis projection is actually suited to, and with five items instead of 558 its failure modes stop mattering.
- Check it with the numbers above, which is what makes this tractable to
verify: total gap should collapse toward the five real discontinuities, and
check_elevation_gain.py's step-plausibility report should stop finding impossible steps anywhere except those five.
Fetch the centerline (lib/arcgis.fetch_layer_to_file off sources.json),
then in DuckDB with spatial, reusing the module's own functions:
parts = reproject_lines_to_meters(con, ordered_oriented_parts(load_merged_trail_line(con, CENTERLINE_PATH)))Everything above is arithmetic on parts — endpoint distances for the gaps,
union-find over endpoints rounded to 1 m for the components, a Counter over
those same rounded endpoints for the degrees.