use geo::{Geometry, HaversineDistance, Point};
use rusqlite::Connection;
use std::collections::HashMap;
use super::{house_number_match, query_house_number};
use crate::domain::admin_level::level;
const MATCH_MAX_DISTANCE_IN_METERS: f64 = 50.0;
const MAX_HOUSE_NUMBER_DIGITS: usize = 5;
fn house_number_core(token: &str) -> Option<&str> {
let core = token.strip_suffix(',').unwrap_or(token);
is_house_number_token(core).then_some(core)
}
fn is_house_number_token(token: &str) -> bool {
if token.is_empty() {
return false;
}
let bytes = token.as_bytes();
let digits = bytes.iter().take_while(|b| b.is_ascii_digit()).count();
if digits == 0 || digits > MAX_HOUSE_NUMBER_DIGITS {
return false;
}
match bytes.len() - digits {
0 => true,
1 => bytes[digits].is_ascii_alphabetic(),
_ => false,
}
}
fn parse_leading_number(s: &str) -> Option<u32> {
let digits: String = s.chars().take_while(|c| c.is_ascii_digit()).collect();
digits.parse().ok()
}
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,
}
}
pub fn enrich_house_number_from_query(
conn: &Connection,
query: &str,
matches: &mut [super::query_match],
friendly_name_format: Option<&str>,
) {
let query_tokens: Vec<&str> = query.split_whitespace().collect();
if !query_tokens.iter().any(|t| house_number_core(t).is_some()) {
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 raw_hns = crate::database::house_numbers::by_admin_level_ids(conn, &admin_level_ids);
let mut hns_by_admin_level: HashMap<i64, Vec<crate::database::house_numbers::hn_for_street>> =
HashMap::new();
for hn in raw_hns {
hns_by_admin_level.entry(hn.admin_level_id).or_default().push(hn);
}
for m in matches.iter_mut() {
let admin_level_id = match m.admin_level_id {
Some(id) => id,
None => continue,
};
let street_name = street_name_of(m);
let number = match first_house_number(&query_tokens, &street_name) {
Some(n) => n,
None => continue,
};
let resolved = hns_by_admin_level.get(&admin_level_id).and_then(|hns| {
exact_point(hns, &number.trim().to_lowercase())
.map(|pt| (house_number_match::exact, pt))
.or_else(|| {
parse_leading_number(&number)
.and_then(|t| interpolate_point(hns, t))
.map(|pt| (house_number_match::interpolated, pt))
})
});
let kind = match resolved {
Some((kind, pt)) => {
m.latitude = super::round5(pt.y());
m.longitude = super::round5(pt.x());
m.admin_levels.push(super::admin_level {
level: level::house_number.value(),
name: number.clone(),
osm_relation_id: None,
osm_way_id: None,
wkt: None,
});
m.friendly_name = match friendly_name_format {
Some(fmt) => super::render_friendly_name(fmt, &m.admin_levels),
None => super::default_friendly_name(&m.admin_levels),
};
if let Some(s) = m.similarity {
m.similarity = Some(super::round5(s as f64 + 0.01) as f32);
}
kind
}
None => house_number_match::absent,
};
m.house_number = Some(query_house_number { number, kind });
}
}
fn first_house_number(query_tokens: &[&str], street_name: &str) -> Option<String> {
query_tokens
.iter()
.copied()
.filter(|t| !name_contains_token(street_name, t))
.find_map(|t| house_number_core(t).map(str::to_string))
}
fn street_name_of(m: &super::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()
}
fn name_contains_token(name: &str, number: &str) -> bool {
name
.split(|c: char| !c.is_alphanumeric())
.any(|t| t.eq_ignore_ascii_case(number))
}
fn exact_point(
hns: &[crate::database::house_numbers::hn_for_street],
wanted: &str,
) -> Option<Point<f64>> {
hns
.iter()
.filter(|hn| hn.number.trim().to_lowercase() == wanted)
.find_map(|hn| point_of(hn.wkb.as_ref()))
}
fn interpolate_point(
hns: &[crate::database::house_numbers::hn_for_street],
target: u32,
) -> Option<Point<f64>> {
let mut below: Option<(u32, Point<f64>)> = None;
let mut above: Option<(u32, Point<f64>)> = None;
for hn in hns {
let value = match parse_leading_number(&hn.number) {
Some(v) => v,
None => continue,
};
let pt = match point_of(hn.wkb.as_ref()) {
Some(p) => p,
None => continue,
};
if value < target {
if below.is_none_or(|(b, _)| value > b) {
below = Some((value, pt));
}
} else if value > target && above.is_none_or(|(a, _)| value < a) {
above = Some((value, pt));
}
}
match (below, above) {
(Some((b, bp)), Some((a, ap))) => {
let frac = (target - b) as f64 / (a - b) as f64;
Some(Point::new(bp.x() + (ap.x() - bp.x()) * frac, bp.y() + (ap.y() - bp.y()) * frac))
}
_ => None,
}
}
pub fn enrich_house_numbers(
conn: &Connection,
input_pt: Point<f64>,
matches: &mut [super::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 raw_hns = crate::database::house_numbers::by_admin_level_ids(conn, &admin_level_ids);
let mut hns_by_admin_level: HashMap<i64, Vec<crate::database::house_numbers::hn_for_street>> =
HashMap::new();
for hn in raw_hns {
hns_by_admin_level.entry(hn.admin_level_id).or_default().push(hn);
}
for m in matches.iter_mut() {
let admin_level_id = match m.admin_level_id {
Some(id) => id,
None => continue,
};
let closest = hns_by_admin_level.get(&admin_level_id).and_then(|hns| {
hns
.iter()
.filter_map(|hn| {
let pt = match hn.wkb.as_ref().map(|g| g.geometry()) {
Some(Geometry::Point(p)) => *p,
_ => return None,
};
let d = input_pt.haversine_distance(&pt);
if d <= MATCH_MAX_DISTANCE_IN_METERS {
Some((hn, d))
} else {
None
}
})
.min_by(|(_, d1), (_, d2)| d1.partial_cmp(d2).unwrap())
});
if let Some((hn, _)) = closest {
m.admin_levels.push(super::admin_level {
level: level::house_number.value(),
name: hn.number.clone(),
osm_relation_id: None,
osm_way_id: None,
wkt: None,
});
m.friendly_name = match friendly_name_format {
Some(fmt) => super::render_friendly_name(fmt, &m.admin_levels),
None => super::default_friendly_name(&m.admin_levels),
};
}
}
}
#[cfg(test)]
#[path = "house_number.test.rs"]
mod tests;