Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/korp-collectors

src/osm_admin.rs

//! **Administrative-boundaries facet** — a "borders-only" world map layer built
//! from OSM, and the data behind a **trie over the world's admin divisions**
//! (world → country → region → … → municipality). This is the analysis +
//! plumbing for the korp connector that feeds a facett borders facet (Sweden's
//! 290 kommuner + 21 regioner/län, Norway's fylke+kommune, Finland's
//! maakunta+kunta, … then all of Europe, then the world).
//!
//! ## What OSM gives (and what it doesn't)
//! Admin areas are OSM **`boundary=administrative` relations** carrying
//! `admin_level` (the depth), `name` / `name:en`, `ISO3166-2`, `ref`, `wikidata`,
//! sometimes `population`. The **per-country meaning of `admin_level` differs** —
//! see [`country_division_name`]. OSM does **not** store a clean parent→child
//! tree: the hierarchy must be derived (geometric containment or `wikidata`/
//! relation membership) — that's what [`WorldAdminTrie`] is for. Authoritative
//! Swedish kommun codes/boundaries still come from Lantmäteriet/SCB; OSM is the
//! uniform *worldwide* approximation, re-imported periodically (skade snapshots
//! give the boundary history / time-travel for free).
//!
//! This module is pure (operates on a feature's **tags JSON**, way or relation),
//! so the filter + trie are tested offline; the PBF read lives in
//! [`super::osm`] behind `--features collectors-osm`.

use std::collections::BTreeMap;

use anyhow::Result;
use arrow_array::RecordBatch;
use arrow_schema::{DataType, Schema};
use serde_json::Value;

use super::arrow_util::{batch, bin_col, i64_col, schema, str_col};

/// One administrative unit extracted from an OSM feature.
#[derive(Debug, Clone, PartialEq)]
pub struct AdminUnit {
    pub osm_id: i64,
    pub admin_level: i64,
    pub name: Option<String>,
    pub name_en: Option<String>,
    /// `ISO3166-2` (e.g. `SE-AB`) when present.
    pub iso: Option<String>,
    /// Country code (ISO 3166-1 alpha-2), derived from `ISO3166-1` or the
    /// `ISO3166-2` prefix; the trie's top key.
    pub country: Option<String>,
    /// The per-country name for this `admin_level` (e.g. `"kommun"`).
    pub division: Option<String>,
    pub geometry_wkb: Option<Vec<u8>>,
}

/// Per-country `admin_level` → division name. The crux the user flagged: OSM's
/// `admin_level` is **not** uniform across countries. (Nordic focus first; a
/// generic fallback covers the rest of the world.)
pub fn country_division_name(country: &str, admin_level: i64) -> Option<&'static str> {
    match (country, admin_level) {
        // Sweden: 21 län/regioner (=4), 290 kommuner (=7).
        ("SE", 2) => Some("land"),
        ("SE", 4) => Some("län/region"),
        ("SE", 7) => Some("kommun"),
        ("SE", 9) => Some("stadsdel/distrikt"),
        // Norway: fylke (=4), kommune (=7).
        ("NO", 2) => Some("land"),
        ("NO", 4) => Some("fylke"),
        ("NO", 7) => Some("kommune"),
        // Finland: maakunta/landskap region (=4), seutukunta (=6), kunta (=8).
        ("FI", 2) => Some("maa"),
        ("FI", 4) => Some("maakunta/region"),
        ("FI", 6) => Some("seutukunta"),
        ("FI", 8) => Some("kunta/kommun"),
        // Denmark: region (=4), kommune (=7).
        ("DK", 2) => Some("land"),
        ("DK", 4) => Some("region"),
        ("DK", 7) => Some("kommune"),
        // Iceland: region (=4), sveitarfélag municipality (=6).
        ("IS", 2) => Some("land"),
        ("IS", 4) => Some("landshluti"),
        ("IS", 6) => Some("sveitarfélag"),
        // Generic OSM ladder for the rest of the world.
        (_, 2) => Some("country"),
        (_, 3) => Some("region-group"),
        (_, 4) => Some("state/region"),
        (_, 5) => Some("province/district"),
        (_, 6) => Some("county"),
        (_, 7) => Some("municipality"),
        (_, 8) => Some("municipality"),
        (_, 9) => Some("ward/suburb"),
        (_, 10) => Some("neighbourhood"),
        _ => None,
    }
}

fn tag<'a>(tags: &'a Value, key: &str) -> Option<&'a str> {
    tags.get(key).and_then(|v| v.as_str())
}

/// Extract an [`AdminUnit`] from an OSM feature's tags JSON — `None` unless it is
/// a `boundary=administrative` feature with a parseable `admin_level`.
pub fn admin_unit_from_tags(
    osm_id: i64,
    tags_json: &str,
    geometry: Option<Vec<u8>>,
) -> Option<AdminUnit> {
    let tags: Value = serde_json::from_str(tags_json).ok()?;
    if tag(&tags, "boundary") != Some("administrative") {
        return None;
    }
    let admin_level: i64 = tag(&tags, "admin_level")?.trim().parse().ok()?;

    let iso = tag(&tags, "ISO3166-2").map(str::to_string);
    let country = tag(&tags, "ISO3166-1").map(str::to_string).or_else(|| {
        iso.as_deref()
            .and_then(|s| s.split('-').next())
            .map(str::to_string)
    });
    let division = country
        .as_deref()
        .and_then(|c| country_division_name(c, admin_level))
        .or_else(|| country_division_name("", admin_level))
        .map(str::to_string);

    Some(AdminUnit {
        osm_id,
        admin_level,
        name: tag(&tags, "name").map(str::to_string),
        name_en: tag(&tags, "name:en").map(str::to_string),
        iso,
        country,
        division,
        geometry_wkb: geometry,
    })
}

/// Filter a batch of OSM features (id, tags_json, geometry) to admin units.
pub fn extract_admin_units(
    features: impl IntoIterator<Item = (i64, String, Option<Vec<u8>>)>,
) -> Vec<AdminUnit> {
    features
        .into_iter()
        .filter_map(|(id, tags, geom)| admin_unit_from_tags(id, &tags, geom))
        .collect()
}

/// The `admin_units` table schema.
pub fn admin_schema() -> Schema {
    schema(&[
        ("osm_id", DataType::Int64, false),
        ("admin_level", DataType::Int64, false),
        ("name", DataType::Utf8, true),
        ("name_en", DataType::Utf8, true),
        ("iso", DataType::Utf8, true),
        ("country", DataType::Utf8, true),
        ("division", DataType::Utf8, true),
        ("geometry_wkb", DataType::Binary, true),
    ])
}

/// Admin units → one Arrow `RecordBatch` (WKB borders for the facet).
pub fn admin_batch(units: &[AdminUnit]) -> Result<RecordBatch> {
    let cols = vec![
        i64_col(&units.iter().map(|u| Some(u.osm_id)).collect::<Vec<_>>()),
        i64_col(
            &units
                .iter()
                .map(|u| Some(u.admin_level))
                .collect::<Vec<_>>(),
        ),
        str_col(&units.iter().map(|u| u.name.clone()).collect::<Vec<_>>()),
        str_col(&units.iter().map(|u| u.name_en.clone()).collect::<Vec<_>>()),
        str_col(&units.iter().map(|u| u.iso.clone()).collect::<Vec<_>>()),
        str_col(&units.iter().map(|u| u.country.clone()).collect::<Vec<_>>()),
        str_col(&units.iter().map(|u| u.division.clone()).collect::<Vec<_>>()),
        bin_col(
            &units
                .iter()
                .map(|u| u.geometry_wkb.clone())
                .collect::<Vec<_>>(),
        ),
    ];
    batch(&admin_schema(), cols)
}

/// The "trie over the world": country → admin_level → unit names. A coarse index
/// for the borders facet's drill-down (world ▸ Nordics ▸ Sweden ▸ län ▸ kommun).
/// (Geometric parent→child nesting is the heavier follow-up; this groups by the
/// derived `country` + `admin_level`, the cheap structural skeleton.)
#[derive(Debug, Default)]
pub struct WorldAdminTrie {
    /// country code → (admin_level → unit display names).
    pub by_country: BTreeMap<String, BTreeMap<i64, Vec<String>>>,
}

impl WorldAdminTrie {
    pub fn build(units: &[AdminUnit]) -> Self {
        let mut t = WorldAdminTrie::default();
        for u in units {
            let country = u.country.clone().unwrap_or_else(|| "??".into());
            let name = u
                .name
                .clone()
                .unwrap_or_else(|| format!("osm/{}", u.osm_id));
            t.by_country
                .entry(country)
                .or_default()
                .entry(u.admin_level)
                .or_default()
                .push(name);
        }
        t
    }

    /// Count of units at `admin_level` in `country`.
    pub fn count(&self, country: &str, admin_level: i64) -> usize {
        self.by_country
            .get(country)
            .and_then(|m| m.get(&admin_level))
            .map_or(0, Vec::len)
    }

    /// Number of distinct countries seen.
    pub fn countries(&self) -> usize {
        self.by_country.len()
    }
}