use crate::domain::admin_level::level;
use super::house_number::enrich_house_numbers;
use geo::{Closest, ClosestPoint, Geometry, HaversineDistance, LineString, Point};
use rusqlite::Connection;
const RTREE_DELTA_DEG: f64 = 0.1;
const WORLD_BOUNDING_BOX: super::bounding_box = super::bounding_box {
min_lat: -90.0,
max_lat: 90.0,
min_lon: -180.0,
max_lon: 180.0,
};
struct admin_candidate {
id: i64,
admin_level: level,
closest_point: Point<f64>,
distance_in_meters: Option<u32>,
}
fn best_admin_levels(
conn: &Connection,
input_pt: Point<f64>,
bounding_wkt: Option<&super::bounding_geometry>,
) -> Vec<admin_candidate> {
let (lon, lat) = (input_pt.x(), input_pt.y());
let envelope = bounding_wkt.map(|b| b.envelope).unwrap_or(WORLD_BOUNDING_BOX);
let raw = crate::domain::admin_level::repository::streets_for_coordinates(
conn,
lon,
lat,
RTREE_DELTA_DEG,
envelope,
);
crate::debug!("debug: rtree raw={} for lon={} lat={}", raw.len(), lon, lat);
let mut rej_wkt_none = 0;
let mut rej_not_linestring = 0;
let mut rej_empty = 0;
let mut rej_indeterminate = 0;
let mut rej_outside_polygon = 0;
let mut min_dist = f64::MAX;
let mut candidates: Vec<admin_candidate> = Vec::new();
for s in raw {
let geom = match s.wkb {
Some(g) => g.into_geometry(),
None => {
rej_wkt_none += 1;
continue;
}
};
let linestrings: Vec<LineString<f64>> = match geom {
Geometry::LineString(ls) => vec![ls],
Geometry::MultiLineString(mls) => mls.0,
_ => {
rej_not_linestring += 1;
continue;
}
};
let mut best: Option<(Point<f64>, f64)> = None;
let mut had_non_empty = false;
for ls in &linestrings {
if ls.0.is_empty() {
continue;
}
had_non_empty = true;
if let Closest::SinglePoint(p) | Closest::Intersection(p) = ls.closest_point(&input_pt) {
let d = input_pt.haversine_distance(&p);
if best.is_none_or(|(_, bd)| d < bd) {
best = Some((p, d));
}
}
}
if !had_non_empty {
rej_empty += 1;
continue;
}
let (cp, dist) = match best {
Some(b) => b,
None => {
rej_indeterminate += 1;
continue;
}
};
if let Some(b) = bounding_wkt
&& !b.contains(cp.y(), cp.x())
{
rej_outside_polygon += 1;
continue;
}
if dist < min_dist {
min_dist = dist;
}
let distance_in_meters = Some(dist.round().clamp(0.0, u32::MAX as f64) as u32);
candidates.push(admin_candidate {
id: s.id,
admin_level: s.admin_level,
closest_point: cp,
distance_in_meters,
});
}
crate::debug!(
"debug: rejections wkt_none={} not_linestring={} empty={} indeterminate={} outside_polygon={} min_dist_m={:.2}",
rej_wkt_none,
rej_not_linestring,
rej_empty,
rej_indeterminate,
rej_outside_polygon,
min_dist
);
candidates.sort_by(|a, b| {
b.admin_level
.cmp(&a.admin_level)
.then(a.distance_in_meters.cmp(&b.distance_in_meters))
});
candidates
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn run(
conn: &Connection,
lat: f64,
lon: f64,
friendly_name_format: Option<&str>,
bounding_wkt: Option<&super::bounding_geometry>,
min_quality: Option<f64>,
last_admin_levels: Option<&[level]>,
include_wkt: bool,
) -> super::query_output {
let input_pt = Point::new(lon, lat);
let mut candidates = best_admin_levels(conn, input_pt, bounding_wkt);
if let Some(threshold) = min_quality {
candidates.retain(|c| super::coordinate_quality(c.distance_in_meters) >= threshold);
}
if last_admin_levels.is_none() {
candidates.truncate(super::MAX_RESULTS as usize);
}
crate::debug!(
"debug: candidates={} raw_query=({}, {})",
candidates.len(),
lat,
lon
);
let candidate_ids: Vec<i64> = candidates.iter().map(|c| c.id).collect();
let hierarchies = crate::domain::admin_level_hierarchy::repository::load_by_ids(conn, &candidate_ids);
let mut all_ancestor_ids: Vec<i64> = hierarchies
.values()
.flat_map(|h| h.ancestor_ids.iter().copied())
.collect();
all_ancestor_ids.sort_unstable();
all_ancestor_ids.dedup();
let mut meta_ids = candidate_ids.clone();
meta_ids.extend(all_ancestor_ids.iter().copied());
meta_ids.sort_unstable();
meta_ids.dedup();
let meta_map = crate::domain::admin_level::repository::load_metadata_by_ids(conn, &meta_ids);
let wkt_by_id = super::load_wkt_by_ids(conn, &meta_ids, include_wkt);
let mut matches: Vec<super::query_match> = Vec::new();
for c in &candidates {
let cp = c.closest_point;
let candidate_meta = meta_map.get(&c.id);
let candidate_name = candidate_meta.map(|m| m.name.clone()).unwrap_or_default();
let hierarchy = hierarchies.get(&c.id);
let base_name = hierarchy
.map(|h| h.user_friendly_name.clone())
.unwrap_or_else(|| candidate_name.clone());
let ancestor_ids = hierarchy.map(|h| h.ancestor_ids.as_slice()).unwrap_or(&[]);
let mut ancestors_sorted: Vec<&crate::domain::admin_level::repository::admin_meta_row> = ancestor_ids
.iter()
.rev()
.filter_map(|id| meta_map.get(id))
.collect();
ancestors_sorted.sort_by_key(|a| a.admin_level);
let mut admin_levels: Vec<super::admin_level> = ancestors_sorted
.iter()
.map(|a| super::admin_level {
level: a.admin_level.value(),
name: a.name.clone(),
osm_relation_id: a.relation_id,
osm_way_id: a.way_id,
wkt: wkt_by_id.get(&a.id).cloned(),
})
.collect();
admin_levels.push(super::admin_level {
level: c.admin_level.value(),
name: candidate_name.clone(),
osm_relation_id: candidate_meta.and_then(|m| m.relation_id),
osm_way_id: candidate_meta.and_then(|m| m.way_id),
wkt: wkt_by_id.get(&c.id).cloned(),
});
let friendly_name = match friendly_name_format {
Some(fmt) => super::render_friendly_name(fmt, &admin_levels),
None => base_name.clone(),
};
let country_iso = ancestors_sorted
.iter()
.copied()
.chain(candidate_meta)
.find(|a| a.admin_level == level::country)
.and_then(|a| a.country_iso_code.clone());
let post_code = ancestors_sorted
.iter()
.filter(|a| a.post_code.is_some())
.max_by_key(|a| a.admin_level)
.and_then(|a| a.post_code.clone())
.or_else(|| candidate_meta.and_then(|m| m.post_code.clone()));
matches.push(super::query_match {
admin_levels,
latitude: super::round5(cp.y()),
longitude: super::round5(cp.x()),
coordinates_distance_in_meters: c.distance_in_meters,
similarity: None,
score: None,
friendly_name,
attributes: super::query_match_attributes {
country_iso_3166_1_alpha_2_code: country_iso,
post_code,
},
house_number: None,
id: c.id as u64,
admin_level_id: Some(c.id),
});
}
enrich_house_numbers(conn, input_pt, &mut matches, friendly_name_format);
super::apply_filters_and_truncate(&mut matches, min_quality, bounding_wkt, last_admin_levels);
super::query_output {
service: super::query_service::coordinates_to_address,
matches,
}
}
#[cfg(test)]
#[path = "coordinates.test.rs"]
mod tests;