a-b-street/abstreet · error

Expected exactly 1 feature

Error message

Expected exactly 1 feature

What it means

After collecting features from the GeoJSON (Feature or FeatureCollection), load_boundary requires exactly one feature because the boundary must be a single polygon. If the file contains zero features (empty collection) or more than one, it bails with 'Expected exactly 1 feature'.

Solutions

  1. Inspect the file's features array; merge multiple polygons into a single MultiPolygon feature, or delete extras so exactly one remains.
  2. If empty, re-download/regenerate the boundary for the correct region.
  3. Wrap multiple polygons into one MultiPolygon geometry so the file still has one feature.

Example fix

// before
{"type": "FeatureCollection", "features": [f1, f2]}
// after
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "MultiPolygon", "coordinates": [f1.geom, f2.geom]}}]}
Defensive patterns

Strategy: validation

Validate before calling

let features: Vec<_> = match &gj {
    GeoJson::Feature(f) => vec![f],
    GeoJson::FeatureCollection(fc) => &fc.features,
    _ => bail!("not a feature/collection"),
};
if features.len() != 1 { bail!("boundary file must contain exactly one feature, got {}", features.len()); }

Type guard

fn single_feature(gj: &GeoJson) -> Option<&geojson::Feature> {
    match gj {
        GeoJson::Feature(f) => Some(f),
        GeoJson::FeatureCollection(fc) if fc.features.len() == 1 => fc.features.first(),
        _ => None,
    }
}

Try / catch

let poly = match load_boundary(path) {
    Ok(p) => p,
    Err(e) if e.to_string().contains("exactly 1 feature") => {
        eprintln!("Merge the boundary into a single feature: {e}");
        return Err(e);
    }
    Err(e) => return Err(e),
};

Prevention

When it happens

Trigger: pick_geofabrik given a boundary.geojson whose FeatureCollection has an empty features array, or multiple features (e.g. multi-part boundary exported as separate features).

Common situations: Downloading a clipped OSM boundary that was exported as multiple polygons; a nearly-empty GeoJSON file created by an export filter that matched nothing.

Understand the failure class

Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.

Related errors


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

Appendix: source

Thrown at importer/src/pick_geofabrik.rs:45

    // Contains some directory structure, like north-america/us/wyoming-latest.osm.pbf or
    // asia/yemen-latest.osm.pbf
    let basename = url
        .strip_prefix("https://download.geofabrik.de/")
        .expect("Geofabrik URLs changed");
    let local = abstio::path_shared_input(format!("geofabrik/{basename}"));

    Ok((url, local))
}

fn load_boundary(path: String) -> Result<geo::Polygon> {
    let gj: GeoJson = abstio::maybe_read_json(path, &mut Timer::throwaway())?;
    let mut features = match gj {
        GeoJson::Feature(feature) => vec![feature],
        GeoJson::FeatureCollection(feature_collection) => feature_collection.features,
        _ => bail!("Unexpected geojson: {:?}", gj),
    };
    if features.len() != 1 {
        bail!("Expected exactly 1 feature");
    }
    let poly: geo::Polygon = features
        .pop()
        .unwrap()
        .geometry
        .take()
        .unwrap()
        .value
        .try_into()
        .unwrap();
    Ok(poly)
}

async fn load_remote_geojson(path: String, url: &str) -> Result<GeoJson> {
    if !abstio::file_exists(&path) {
        info!("Downloading {}", url);
        abstio::download_to_file(url, None, &path).await?;
    }

View on GitHub (pinned to 0964f29315)