use geo::{Geometry, HaversineDistance, Point};
use rusqlite::Connection;
use std::collections::HashMap;
use super::entity::{house_number_match, query_house_number, query_match, round5};
use crate::domain::admin_level::level;
use crate::domain::house_number::{
house_number, house_number_policy, house_number_resolution, resolution, token,
};
const MATCH_MAX_DISTANCE_IN_METERS: f64 = 50.0;
fn point_of(wkb: Option<&crate::domain::admin_level::geometry::admin_geometry>) -> Option<Point<f64>> {
match wkb.map(|g| g.geometry()) {
Some(Geometry::Point(p)) => Some(*p),
_ => None,
}
}
fn numbers_by_street(
conn: &Connection,
admin_level_ids: &[i64],
) -> HashMap<i64, Vec<(house_number, Point<f64>)>> {
let mut by_street: HashMap<i64, Vec<(house_number, Point<f64>)>> = HashMap::new();
for row in crate::domain::house_number::repository::by_admin_level_ids(conn, admin_level_ids) {
if let Some(point) = point_of(row.wkb.as_ref()) {
by_street
.entry(row.admin_level_id)
.or_default()
.push((row.number, point));
}
}
by_street
}
fn place_on_house_number(
m: &mut query_match,
point: Point<f64>,
number: &house_number,
friendly_name_format: Option<&str>,
) {
m.latitude = round5(point.y());
m.longitude = round5(point.x());
m.append_house_number_level(number.stored_form(), friendly_name_format);
if let Some(s) = m.similarity {
m.similarity = Some(round5(s as f64 + 0.01) as f32);
}
}
pub(super) fn enrich_house_number_from_query(
conn: &Connection,
query: &str,
matches: &mut [query_match],
friendly_name_format: Option<&str>,
policy: &house_number_policy,
) {
if !token::has_house_number(query, policy) {
return;
}
let admin_level_ids: Vec<i64> = matches.iter().filter_map(|m| m.admin_level_id).collect();
if admin_level_ids.is_empty() {
return;
}
let by_street = numbers_by_street(conn, &admin_level_ids);
let no_numbers: Vec<(house_number, Point<f64>)> = Vec::new();
for m in matches.iter_mut() {
let Some(admin_level_id) = m.admin_level_id else {
continue;
};
let street_name = street_name_of(m);
let Some(number) = token::first_house_number(query, &street_name, policy) else {
continue;
};
let known = by_street.get(&admin_level_id).unwrap_or(&no_numbers);
let kind = match resolution::resolve(&number, known) {
house_number_resolution::exact(point) => {
place_on_house_number(m, point, &number, friendly_name_format);
house_number_match::exact
}
house_number_resolution::interpolated(point) => {
place_on_house_number(m, point, &number, friendly_name_format);
house_number_match::interpolated
}
house_number_resolution::absent => house_number_match::absent,
};
m.house_number = Some(query_house_number {
number: number.stored_form().to_string(),
kind,
});
}
}
fn street_name_of(m: &query_match) -> String {
let street = level::street.value();
m.admin_levels
.iter()
.find(|a| a.level == street)
.map(|a| a.name.clone())
.unwrap_or_default()
}
pub(super) fn enrich_house_numbers(
conn: &Connection,
input_pt: Point<f64>,
matches: &mut [query_match],
friendly_name_format: Option<&str>,
) {
let admin_level_ids: Vec<i64> = matches.iter().filter_map(|m| m.admin_level_id).collect();
if admin_level_ids.is_empty() {
return;
}
let by_street = numbers_by_street(conn, &admin_level_ids);
for m in matches.iter_mut() {
let Some(admin_level_id) = m.admin_level_id else {
continue;
};
let closest = by_street.get(&admin_level_id).and_then(|numbers| {
numbers
.iter()
.filter_map(|(number, point)| {
let distance = input_pt.haversine_distance(point);
(distance <= MATCH_MAX_DISTANCE_IN_METERS).then_some((number, distance))
})
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
});
if let Some((number, _)) = closest {
let name = number.stored_form().to_string();
m.append_house_number_level(&name, friendly_name_format);
}
}
}