geolite 0.0.18

open source geocode solution
use std::collections::{BTreeSet, HashMap};

use geo::Point;
use rusqlite::Connection;
use serde::Serialize;
use utoipa::ToSchema;

use super::id::address_id;
use super::label;
use crate::admin_level::geometry::bounding_box;
use crate::admin_level::level;
use crate::admin_level::repository::admin_meta_row;
use crate::admin_level_hierarchy::paths::paths_of;
use crate::admin_level_hierarchy::tantivy_index;
use crate::house_number::house_number_scenario;

#[derive(Serialize, ToSchema)]
pub enum query_service {
  coordinates_to_address,
  text_to_address,
}

#[derive(Serialize, ToSchema)]
pub struct admin_level {
  pub level: u8,
  pub name: String,
  pub post_code: Option<String>,
  pub osm_relation_id: Option<u64>,
  pub osm_way_id: Option<u64>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub osm_merged_way_ids: Option<Vec<u64>>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub wkt: Option<String>,
}

#[derive(Serialize, ToSchema)]
pub struct query_match_attributes {
  pub country_iso_3166_1_alpha_2_code: Option<String>,
  pub post_code: Option<String>,
}

#[derive(Clone, Serialize, ToSchema)]
pub struct query_house_number {
  pub number: String,
  pub kind: house_number_scenario,
  pub osm_node_ids: Vec<u64>,
  pub meters_per_number: Option<f64>,
}

#[derive(Serialize, ToSchema)]
pub struct query_match {
  pub admin_levels: Vec<admin_level>,
  pub latitude: f64,
  pub longitude: f64,
  pub coordinates_distance_in_meters: Option<u32>,
  pub similarity: Option<f32>,
  pub score: Option<f32>,
  pub friendly_name: String,
  pub attributes: query_match_attributes,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub house_number: Option<query_house_number>,
  pub id: String,
  #[serde(skip)]
  pub(super) ladder_ids: Vec<i64>,
}

#[derive(Serialize, ToSchema)]
pub struct database_status {
  pub file: String,
  pub text_to_address: bool,
  pub coordinates_to_address: bool,
}

impl database_status {
  // structural probe: `prepare` compiles the table reference, so this is Ok iff admin_levels_rtree
  // exists — the distinguishing dependency of the reverse-geocoding path. no row i/o.
  pub fn of(file: &str, conn: &Connection, index: Option<&tantivy_index>) -> Self {
    const SQL_COORDINATES_PROBE: &str = "
      SELECT 1
      FROM admin_levels_rtree
      LIMIT 1
    ";

    database_status {
      file: file.to_string(),
      text_to_address: index.is_some(),
      coordinates_to_address: conn.prepare(SQL_COORDINATES_PROBE).is_ok(),
    }
  }
}

#[derive(Serialize, ToSchema)]
pub struct query_output {
  pub service: query_service,
  pub databases: Vec<database_status>,
  pub matches: Vec<query_match>,
}

pub(super) fn attach_wkt(conn: &Connection, matches: &mut [&mut query_match]) {
  let mut ids: Vec<i64> = matches.iter().flat_map(|m| m.ladder_ids.iter().copied()).collect();
  ids.sort_unstable();
  ids.dedup();
  let wkt = crate::admin_level::repository::wkt_by_ids(conn, &ids);
  for m in matches.iter_mut() {
    let query_match {
      admin_levels,
      ladder_ids,
      ..
    } = &mut **m;
    for (level, id) in admin_levels.iter_mut().zip(ladder_ids.iter()) {
      level.wkt = wkt.get(id).cloned();
    }
  }
}

pub(super) fn round5(v: f64) -> f64 {
  (v * 100_000.0).round() / 100_000.0
}

pub(super) struct leaf<'a> {
  pub(super) id: i64,
  pub(super) level: level,
  pub(super) name: &'a str,
  pub(super) relation_id: Option<u64>,
  pub(super) way_id: Option<u64>,
}

pub(super) struct match_sources {
  iso: String,
  paths: HashMap<i64, Vec<Vec<i64>>>,
  pub(super) meta: HashMap<i64, admin_meta_row>,
  merged_way_ids: HashMap<i64, crate::admin_level::geometry::merged_way_ids>,
  boxes: HashMap<i64, bounding_box>,
}

impl match_sources {
  pub(super) fn load(conn: &Connection, iso: &str, ids: &[i64]) -> Self {
    let edges = crate::admin_level_hierarchy::repository::ancestry_of(conn, ids);
    let paths: HashMap<i64, Vec<Vec<i64>>> =
      ids.iter().map(|&id| (id, paths_of(id, &edges))).collect();
    let mut meta_ids: Vec<i64> = ids.to_vec();
    meta_ids.extend(paths.values().flatten().flatten().copied());
    meta_ids.sort_unstable();
    meta_ids.dedup();
    let meta = crate::admin_level::repository::load_metadata_by_ids(conn, &meta_ids);
    let merged_way_ids = crate::admin_level::repository::merged_way_ids_by_ids(conn, &meta_ids);
    match_sources {
      iso: iso.to_string(),
      paths,
      meta,
      merged_way_ids,
      boxes: HashMap::new(),
    }
  }

  fn merged_way_ids_of(&self, id: i64) -> Option<Vec<u64>> {
    self.merged_way_ids.get(&id).map(|ways| ways.0.clone())
  }

  pub(super) fn load_leaf_boxes(&mut self, conn: &Connection) {
    let leaves: BTreeSet<i64> = self
      .paths
      .values()
      .filter(|paths| paths.len() > 1)
      .flatten()
      .filter_map(|path| path.first().copied())
      .collect();
    let leaves: Vec<i64> = leaves.into_iter().collect();
    self.boxes = crate::admin_level::repository::boxes_by_ids(conn, &leaves);
  }

  pub(super) fn center_of(&self, id: i64) -> Option<Point<f64>> {
    self.boxes.get(&id).map(bounding_box::center)
  }

  pub(super) fn box_covers(&self, id: i64, point: &Point<f64>) -> bool {
    self.boxes.get(&id).is_some_and(|mbr| mbr.covers(point))
  }

  // an area the hierarchy does not know still answers one match, with an empty path
  pub(super) fn paths_of(&self, id: i64) -> &[Vec<i64>] {
    const NO_PATH: &[Vec<i64>] = &[Vec::new()];

    self
      .paths
      .get(&id)
      .filter(|paths| !paths.is_empty())
      .map(Vec::as_slice)
      .unwrap_or(NO_PATH)
  }

  pub(super) fn path_at(&self, id: i64, ordinal: u8) -> Option<&[i64]> {
    self
      .paths
      .get(&id)
      .and_then(|paths| paths.get(ordinal as usize))
      .map(Vec::as_slice)
  }

  pub(super) fn ancestors_of<'a>(
    &'a self,
    chain: impl Iterator<Item = &'a i64>,
  ) -> Vec<&'a admin_meta_row> {
    let mut ancestors: Vec<&admin_meta_row> = chain.filter_map(|id| self.meta.get(id)).collect();
    ancestors.sort_by_key(|a| a.admin_level);
    ancestors
  }

  // the label without a template follows the chain's own order, not the ladder's: the ladder
  // sorts by level and the two services order a level differently, while the label is one
  pub(super) fn default_label(
    &self,
    own_id: i64,
    own_name: &str,
    house_number: Option<&str>,
    chain: &[i64],
  ) -> String {
    let own_post_code = self.meta.get(&own_id).and_then(|m| m.post_code.as_deref());
    label::place_label(
      (own_name, own_post_code),
      house_number,
      chain
        .iter()
        .filter_map(|id| self.meta.get(id))
        .map(|a| (a.name.as_str(), a.post_code.as_deref())),
    )
  }

  // the country of a row is written on it; a row written before the country reached every row
  // reads the one of the country on its path
  pub(super) fn country_iso_of(&self, own_id: i64, chain: &[i64]) -> Option<&str> {
    let own = self.meta.get(&own_id);
    own
      .and_then(|m| m.country_iso_code.as_deref())
      .or_else(|| {
        chain
          .iter()
          .filter_map(|id| self.meta.get(id))
          .chain(own)
          .find(|m| m.admin_level == level::country)
          .and_then(|m| m.country_iso_code.as_deref())
      })
  }

  pub(super) fn house_number_policy_of(&self, own_id: i64) -> crate::house_number::house_number_policy {
    let first_path = self.paths_of(own_id).first().map(Vec::as_slice).unwrap_or(&[]);
    crate::presets::for_country(self.country_iso_of(own_id, first_path)).house_numbers
  }

  pub(super) fn post_code_of(&self, own_id: i64, chain: &[i64]) -> Option<String> {
    std::iter::once(&own_id)
      .chain(chain)
      .filter_map(|id| self.meta.get(id))
      .find_map(|m| m.post_code.clone())
  }

  fn level_ladder(&self, ancestors: &[&admin_meta_row], leaf: &leaf) -> (Vec<admin_level>, Vec<i64>) {
    let mut admin_levels: Vec<admin_level> = ancestors
      .iter()
      .map(|a| admin_level {
        level: a.admin_level.value(),
        name: a.name.clone(),
        post_code: a.post_code.clone(),
        osm_relation_id: a.relation_id,
        osm_way_id: a.way_id,
        osm_merged_way_ids: self.merged_way_ids_of(a.id),
        wkt: None,
      })
      .collect();
    admin_levels.push(admin_level {
      level: leaf.level.value(),
      name: leaf.name.to_string(),
      post_code: self.meta.get(&leaf.id).and_then(|m| m.post_code.clone()),
      osm_relation_id: leaf.relation_id,
      osm_way_id: leaf.way_id,
      osm_merged_way_ids: self.merged_way_ids_of(leaf.id),
      wkt: None,
    });
    let ids = ancestors.iter().map(|a| a.id).chain(std::iter::once(leaf.id)).collect();
    (admin_levels, ids)
  }

  pub(super) fn match_at(
    &self,
    leaf: &leaf,
    ancestors: &[&admin_meta_row],
    path: &[i64],
    point: Point<f64>,
    house_number: Option<&query_house_number>,
    format: Option<&str>,
  ) -> query_match {
    let (admin_levels, ladder_ids) = self.level_ladder(ancestors, leaf);
    let number = house_number.map(|house_number| house_number.number.as_str());
    let friendly_name = friendly_name_of(
      format,
      &admin_levels,
      self,
      leaf.id,
      leaf.name,
      number,
      path,
    );
    query_match {
      admin_levels,
      latitude: round5(point.y()),
      longitude: round5(point.x()),
      coordinates_distance_in_meters: None,
      similarity: None,
      score: None,
      friendly_name,
      attributes: query_match_attributes {
        country_iso_3166_1_alpha_2_code: self.country_iso_of(leaf.id, path).map(str::to_owned),
        post_code: self.post_code_of(leaf.id, path),
      },
      house_number: house_number.cloned(),
      id: address_id::encode(&self.iso, leaf.id, path, number),
      ladder_ids,
    }
  }
}

fn friendly_name_of(
  format: Option<&str>,
  admin_levels: &[admin_level],
  sources: &match_sources,
  own_id: i64,
  own_name: &str,
  house_number: Option<&str>,
  chain: &[i64],
) -> String {
  match format {
    Some(fmt) => label::render_friendly_name(fmt, admin_levels, house_number),
    None => sources.default_label(own_id, own_name, house_number, chain),
  }
}