a-b-street/abstreet · error

and don't share a common endpoint

Error message

{} and {} don't share a common endpoint

What it means

to_block converts a perimeter into a Block by tracing its boundary lanes; it needs each consecutive pair of lanes to share a common endpoint to stitch the polylines together. When CommonEndpoint::None, the two lanes are disjoint, so the block cannot be built and this error names both lane IDs.

Solutions

  1. Verify the two lane IDs from the message actually meet at a shared intersection in the current map.
  2. Build the Perimeter through the normal blockfinding pipeline rather than hand-constructing it.
  3. Re-run find_blocks on the latest map to refresh cached blocks after road edits.
  4. If maintaining the code, ensure the correct CommonEndpoint variant (FirstFirst/FirstLast/...) is handled so orientation is fixed before tracing.
Defensive patterns

Strategy: validation

Validate before calling

if perimeter.roads.windows(2).any(|w| {
    map.get_r(w[0].road).common_endpoint(map.get_r(w[1].road)) == CommonEndpoint::None
}) {
    return Err(anyhow!("perimeter has disjoint adjacent roads"));
}

Prevention

When it happens

Trigger: Calling Perimeter::to_block (public API) on a perimeter whose step trace encounters two adjacent lanes with no shared endpoint, during block conversion.

Common situations: Calling to_block on a perimeter you built by hand instead of via blockfinding; map edits that disconnected lanes after blocks were found; side-of-driving or step-direction logic misidentifying the shared corner.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of a-b-street/abstreet@0964f29315 (2026-09-13). Data as JSON: /api/errors/356e8554c1476d4a. Report an issue: GitHub.

Appendix: source

Thrown at blockfinding/src/lib.rs:644

            }
            let keep_lane_orientation = if pair[0].road == pair[1].road {
                // We're doubling back at a dead-end. Always follow the orientation of the lane.
                true
            } else {
                match lane1.common_endpoint(lane2) {
                    CommonEndpoint::One(i) => i == lane1.dst_i,
                    CommonEndpoint::Both => {
                        // Two different roads link the same two intersections. I don't think we
                        // can decide the order of points other than seeing which endpoint is
                        // closest to our last point.
                        if let Some(last) = pts.last() {
                            last.dist_to(pl.first_pt()) < last.dist_to(pl.last_pt())
                        } else {
                            // The orientation doesn't matter
                            true
                        }
                    }
                    CommonEndpoint::None => bail!(
                        "{} and {} don't share a common endpoint",
                        lane1.id,
                        lane2.id
                    ),
                }
            };
            if !keep_lane_orientation {
                pl = pl.reversed();
            }

            // Before we add this road's points, try to trace along the polygon's boundary. Usually
            // this has no effect (we'll dedupe points), but sometimes there's an extra curve.
            //
            // Note this logic is similar to how we find SharedSidewalkCorners. Don't rely on that
            // existing, since the outermost lane mightn't be a sidewalk.
            //
            // If the ring.doubles_back(), don't bother. If we tried to trace the boundary, it
            // usually breaks the final Ring we produce. Better to skip bad intersection polygons

View on GitHub (pinned to 0964f29315)