geolite 0.0.3

open source geocode solution
pub mod level_10;
pub mod level_12;

use geo::{Coord, Geometry, LineString, MultiLineString, MultiPolygon, Polygon, Winding};
use rusqlite::Connection;
use std::sync::{Arc, Mutex, mpsc};

#[derive(Clone, Copy)]
pub enum osm_admin_level {
  continent = 1,
  country = 2,
  region = 3,
  state = 4,
  district = 5,
  county = 6,
  municipality = 7,
  city = 8,
  locality = 9,
  neighborhood = 10,
  street = 12,
  address = 14,
  house_numbers = 30,
}

impl TryFrom<u8> for osm_admin_level {
  type Error = u8;
  fn try_from(v: u8) -> Result<Self, Self::Error> {
    match v {
      1 => Ok(osm_admin_level::continent),
      2 => Ok(osm_admin_level::country),
      3 => Ok(osm_admin_level::region),
      4 => Ok(osm_admin_level::state),
      5 => Ok(osm_admin_level::district),
      6 => Ok(osm_admin_level::county),
      7 => Ok(osm_admin_level::municipality),
      8 => Ok(osm_admin_level::city),
      9 => Ok(osm_admin_level::locality),
      10 => Ok(osm_admin_level::neighborhood),
      12 => Ok(osm_admin_level::street),
      14 => Ok(osm_admin_level::address),
      30 => Ok(osm_admin_level::house_numbers),
      _ => Err(v),
    }
  }
}

pub struct progress_report {
  pub total: Option<u64>,
  pub processed: u64,
}

#[derive(Clone, Copy)]
pub struct extraction_rules {
  pub level: u8,
  pub include: &'static [crate::database::osm_ways::filters],
  pub exclude: &'static [crate::database::osm_ways::filters],
}

fn default_include(level: u8) -> &'static [crate::database::osm_ways::filters] {
  use crate::database::osm_ways::filters;
  match level {
    10 => &[
      filters::include_place_neighbourhood,
      filters::include_place_suburb,
    ],
    _ => &[],
  }
}

fn default_exclude(level: u8) -> &'static [crate::database::osm_ways::filters] {
  use crate::database::osm_ways::filters;
  match level {
    12 => &[
      filters::exclude_place_neighbourhood,
      filters::exclude_place_suburb,
      filters::exclude_leisure_park,
      filters::exclude_building,
      filters::exclude_waterway,
    ],
    _ => &[],
  }
}

pub fn resolve_rules(
  level: u8,
  overrides: &[extraction_rules],
) -> (
  &'static [crate::database::osm_ways::filters],
  &'static [crate::database::osm_ways::filters],
) {
  if let Some(r) = overrides.iter().find(|r| r.level == level) {
    return (r.include, r.exclude);
  }
  (default_include(level), default_exclude(level))
}

pub(super) const CHUNK_SIZE: usize = 500;

struct rel_meta {
  name: String,
  country_iso_code: Option<String>,
  post_code: Option<String>,
}

struct rel_work {
  relation_id: u64,
  meta: rel_meta,
  ways: Vec<LineString<f64>>,
}

pub fn run_with_ids(
  conn: &Connection,
  ids: Vec<u64>,
  admin_level: osm_admin_level,
  threads: usize,
  name_priority: &[&str],
  progress: impl Fn(progress_report),
) {
  let total = ids.len() as u64;
  progress(progress_report {
    total: Some(total),
    processed: 0,
  });
  if ids.is_empty() {
    return;
  }

  let (work_tx, work_rx) = mpsc::channel::<rel_work>();
  let work_rx = Arc::new(Mutex::new(work_rx));
  let (result_tx, result_rx) =
    mpsc::channel::<Option<crate::database::admin_levels::admin_levels>>();

  std::thread::scope(|s| {
    for _ in 0..threads {
      let rx = work_rx.clone();
      let tx = result_tx.clone();
      s.spawn(move || {
        loop {
          let item = { rx.lock().unwrap().recv() };
          match item {
            Ok(w) => {
              tx.send(process_one_relation(
                w.relation_id,
                &w.meta,
                &w.ways,
                admin_level,
              ))
              .ok();
            }
            Err(_) => break,
          }
        }
      });
    }
    drop(result_tx);

    let mut processed: u64 = 0;

    for chunk in ids.chunks(CHUNK_SIZE) {
      let dispatched = load_and_send(conn, chunk, name_priority, &work_tx);

      let mut batch: Vec<crate::database::admin_levels::admin_levels> = Vec::new();
      for _ in 0..dispatched {
        if let Ok(Some(row)) = result_rx.recv() {
          batch.push(row);
        }
      }

      processed += crate::database::admin_levels::batch_upsert(conn, &batch) as u64;
      progress(progress_report {
        total: Some(total),
        processed,
      });
    }

    drop(work_tx);
  });
}

fn load_and_send(
  conn: &Connection,
  chunk: &[u64],
  name_priority: &[&str],
  tx: &mpsc::Sender<rel_work>,
) -> usize {
  let raw_rows = crate::database::osm_relations::relation_coords_chunk(conn, chunk, name_priority);

  let mut relation_order: Vec<u64> = Vec::new();
  let mut relation_metas: std::collections::HashMap<u64, rel_meta> =
    std::collections::HashMap::new();
  let mut way_builders: std::collections::HashMap<
    u64,
    std::collections::BTreeMap<String, Vec<Coord<f64>>>,
  > = std::collections::HashMap::new();

  for row in &raw_rows {
    if let std::collections::hash_map::Entry::Vacant(e) = relation_metas.entry(row.relation_id) {
      relation_order.push(row.relation_id);
      e.insert(rel_meta {
        name: row.relation_name.clone(),
        country_iso_code: row.country_iso_code.clone(),
        post_code: row.post_code.clone(),
      });
      way_builders.insert(row.relation_id, std::collections::BTreeMap::new());
    }
    let builder = way_builders.get_mut(&row.relation_id).unwrap();
    let way_key = format!("{:010}_{}", row.way_order, row.way_id);
    builder.entry(way_key).or_default().push(Coord {
      x: row.lon,
      y: row.lat,
    });
  }

  let count = relation_order.len();
  for relation_id in relation_order {
    let meta = relation_metas.remove(&relation_id).unwrap();
    let ways: Vec<LineString<f64>> = way_builders
      .remove(&relation_id)
      .unwrap()
      .into_values()
      .map(LineString)
      .collect();
    tx.send(rel_work {
      relation_id,
      meta,
      ways,
    })
    .ok();
  }
  count
}

fn process_one_relation(
  relation_id: u64,
  meta: &rel_meta,
  ways: &[LineString<f64>],
  admin_level: osm_admin_level,
) -> Option<crate::database::admin_levels::admin_levels> {
  if ways.iter().all(|ls| ls.0.is_empty()) {
    return None;
  }

  let rings = assemble_rings(ways);
  let polygon_rings: Vec<LineString<f64>> = rings
    .into_iter()
    .filter(|r| r.0.len() >= 4 && approx_eq(r.0[0], *r.0.last().unwrap()))
    .collect();

  let geom: Geometry<f64> = if !polygon_rings.is_empty() {
    let polygons: Vec<Polygon<f64>> = polygon_rings
      .into_iter()
      .map(|mut ls| {
        // spatialite st_buildarea reverses ccw rings to cw — replicate that behavior
        ls.make_cw_winding();
        Polygon::new(ls, vec![])
      })
      .collect();
    Geometry::MultiPolygon(MultiPolygon(polygons))
  } else {
    Geometry::MultiLineString(MultiLineString(ways.to_vec()))
  };

  Some(crate::database::admin_levels::admin_levels {
    relation_id: Some(relation_id),
    way_id: None,
    admin_level: admin_level as u8,
    name: meta.name.clone(),
    country_iso_code: meta.country_iso_code.clone(),
    post_code: meta.post_code.clone(),
    wkb: geom.into(),
  })
}

pub(super) fn approx_eq(a: Coord<f64>, b: Coord<f64>) -> bool {
  (a.x - b.x).abs() < 1e-9 && (a.y - b.y).abs() < 1e-9
}

fn assemble_rings(ways: &[LineString<f64>]) -> Vec<LineString<f64>> {
  let mut remaining: Vec<(LineString<f64>, bool)> =
    ways.iter().map(|w| (w.clone(), false)).collect();
  let mut rings: Vec<LineString<f64>> = Vec::new();
  while let Some(start_idx) = remaining.iter().position(|(_, used)| !used) {
    let mut coords: Vec<Coord<f64>> = remaining[start_idx].0.0.clone();
    remaining[start_idx].1 = true;
    let mut extended = true;
    while extended {
      extended = false;
      let tail = *coords.last().unwrap();
      for (ls, used) in remaining.iter_mut() {
        if *used {
          continue;
        }
        let pts = &ls.0;
        if approx_eq(pts[0], tail) {
          coords.extend_from_slice(&pts[1..]);
          *used = true;
          extended = true;
          break;
        }
        if approx_eq(pts[pts.len() - 1], tail) {
          let mut rev = pts.clone();
          rev.reverse();
          coords.extend_from_slice(&rev[1..]);
          *used = true;
          extended = true;
          break;
        }
      }
    }
    rings.push(LineString(coords));
  }
  rings
}

#[cfg(test)]
#[path = "admin_levels.test.rs"]
mod tests;