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
- Inspect the file's features array; merge multiple polygons into a single MultiPolygon feature, or delete extras so exactly one remains.
- If empty, re-download/regenerate the boundary for the correct region.
- 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
- Dissolve multi-part boundaries into one MultiPolygon feature before importing
- Never export empty selections from the GIS tool — verify feature count after export
- Check feature count with jq '.features | length' before running the importer
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
- Unexpected geojson
- study area geojson has
- Input is missing geo_code
- Started on a road we shouldn't trace
- doesn't have a column called Longitude, Latitude, or…
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)