use std::collections::HashMap;
use geo::{Centroid, Closest, ClosestPoint, Geometry, Point};
use rusqlite::Connection;
use super::entity::{leaf, match_sources, query_match, round5};
use super::house_number;
use super::{filter, query_opts};
use crate::admin_level::level;
use crate::admin_level::repository::admin_area_row;
use crate::admin_level_hierarchy::search_index::{
self, search_hit, search_tier, tantivy_index, tokenize,
};
use crate::house_number::{house_number_resolution, house_number_scenario, policy, token};
const MAX_FTS_HITS: u8 = 50;
pub(super) fn region_of(conn: &Connection, opts: &query_opts) -> Option<Vec<i64>> {
opts
.bounding
.as_ref()
.map(|b| crate::admin_level::spatial_index::ids_in_bounding_box(conn, b.envelope))
}
pub(super) fn hits_at(
index: &tantivy_index,
tier: search_tier,
text: &str,
opts: &query_opts,
region: Option<&[i64]>,
) -> Vec<search_hit> {
if region.is_some_and(|ids| ids.is_empty()) {
return vec![];
}
index.search(
tier,
text,
&token::house_number_words(text, &policy::EVERY_SHAPE),
MAX_FTS_HITS as usize,
opts.last_admin_levels.as_deref(),
region,
)
}
pub(super) fn matches_of(
conn: &Connection,
iso: &str,
text: &str,
hits: &[search_hit],
opts: &query_opts,
) -> Vec<query_match> {
let query_tokens = tokenize(text);
let mut ids: Vec<i64> = hits.iter().map(|hit| hit.admin_level_id).collect();
ids.sort_unstable();
ids.dedup();
let mut sources = match_sources::load(conn, iso, &ids);
sources.load_leaf_boxes(conn);
let records = crate::admin_level::repository::load_full_by_ids(conn, &ids);
let record_map: HashMap<i64, &admin_area_row> = records.iter().map(|r| (r.id, r)).collect();
let streets: Vec<&admin_area_row> = records
.iter()
.filter(|r| r.admin_level == level::street)
.collect();
let numbers =
house_number::from_query(conn, text, &streets, |id| sources.house_number_policy_of(id));
let mut matches: Vec<query_match> = hits
.iter()
.filter_map(|hit| {
let record = record_map.get(&hit.admin_level_id)?;
let Some(path) = sources.path_at(hit.admin_level_id, hit.ordinal) else {
crate::debug!(
"debug: hit {} ordinal {} has no path: the index is out of step with the hierarchy",
hit.admin_level_id,
hit.ordinal
);
return None;
};
build_match(record, &sources, &query_tokens, hit, path, numbers.get(&record.id), opts)
})
.collect();
matches.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
b.similarity
.partial_cmp(&a.similarity)
.unwrap_or(std::cmp::Ordering::Equal)
})
});
filter::apply_filters_and_truncate(&mut matches, opts);
matches
}
pub(super) fn resting_point(
record: &admin_area_row,
geometry: &Geometry<f64>,
sources: &match_sources,
path: &[i64],
) -> Option<Point<f64>> {
let centroid = geometry.centroid()?;
if record.admin_level != level::street {
return Some(centroid);
}
let toward = match (sources.paths_of(record.id).len() > 1, path.first()) {
(true, Some(&leaf)) => sources.center_of(leaf).unwrap_or(centroid),
_ => centroid,
};
match geometry.closest_point(&toward) {
Closest::SinglePoint(point) | Closest::Intersection(point) => Some(point),
Closest::Indeterminate => Some(centroid),
}
}
fn build_match(
record: &admin_area_row,
sources: &match_sources,
query_tokens: &[String],
hit: &search_hit,
path: &[i64],
resolution: Option<&house_number_resolution>,
opts: &query_opts,
) -> Option<query_match> {
let geom = record.wkb.as_ref()?.geometry();
let centroid = resting_point(record, geom, sources, path)?;
let point = resolution.map_or(centroid, |placed| placed.point);
let ancestors = sources.ancestors_of(path.iter());
let leaf = leaf {
id: record.id,
level: record.admin_level,
name: &record.name,
relation_id: record.relation_id,
way_id: record.way_id,
};
let own_meta = sources.meta.get(&record.id);
let coverage = search_index::coverage(
query_tokens,
(&record.name, own_meta.and_then(|m| m.post_code.as_deref())),
ancestors.iter().map(|a| (a.name.as_str(), a.post_code.as_deref())),
);
let mut similarity = round5(coverage as f64) as f32;
if resolution.is_some_and(|placed| {
matches!(
placed.origin.scenario(),
house_number_scenario::from_osm_data
| house_number_scenario::presumed_from_multiple_references_from_street
)
}) {
similarity = round5(similarity as f64 + 0.01) as f32;
}
let reported = resolution.map(house_number::reported);
Some(query_match {
similarity: Some(similarity),
score: Some(hit.score),
..sources.match_at(
&leaf,
&ancestors,
path,
point,
reported.as_ref(),
opts.friendly_name_format,
)
})
}