use std::collections::HashMap;
use geo::{Centroid, Closest, ClosestPoint, Geometry, Point};
use rusqlite::Connection;
use super::entity::{
self, leaf, match_sources, query_match, query_match_attributes, query_output, query_service,
round5,
};
use super::house_number::{self, resolved_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, tantivy_index, tokenize};
use crate::house_number::house_number_policy;
const MAX_FTS_HITS: u8 = 50;
pub(super) fn run(
conn: &Connection,
house_numbers: &house_number_policy,
index: &tantivy_index,
text: &str,
opts: &query_opts,
) -> query_output {
let empty = || query_output {
service: query_service::text_to_address,
matches: vec![],
};
let region_ids = opts
.bounding
.as_ref()
.map(|b| crate::admin_level::spatial_index::ids_in_bounding_box(conn, b.envelope));
if region_ids.as_ref().is_some_and(|r| r.is_empty()) {
return empty();
}
let hits = index.search(
text,
MAX_FTS_HITS as usize,
opts.last_admin_levels.as_deref(),
region_ids.as_deref(),
);
if hits.is_empty() {
return empty();
}
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, &ids, opts.include_wkt);
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<(i64, &str)> = records
.iter()
.filter(|r| r.admin_level == level::street)
.map(|r| (r.id, r.name.as_str()))
.collect();
let numbers = house_number::from_query(conn, text, &streets, house_numbers);
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);
query_output {
service: query_service::text_to_address,
matches,
}
}
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],
number: Option<&resolved_number>,
opts: &query_opts,
) -> Option<query_match> {
let geom = record.wkb.as_ref()?.geometry();
let centroid = resting_point(record, geom, sources, path)?;
let placed = number.and_then(resolved_number::placed);
let placed_number = placed.map(|(n, _)| n);
let point = placed.map_or(centroid, |(_, p)| p);
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 admin_levels = sources.level_ladder(&ancestors, &leaf, placed_number);
let friendly_name = entity::friendly_name_of(
opts.friendly_name_format,
&admin_levels,
sources,
record.id,
&record.name,
placed_number,
path,
);
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 placed.is_some() {
similarity = round5(similarity as f64 + 0.01) as f32;
}
Some(query_match {
admin_levels,
latitude: round5(point.y()),
longitude: round5(point.x()),
coordinates_distance_in_meters: None,
similarity: Some(similarity),
score: Some(hit.score),
friendly_name,
attributes: query_match_attributes {
country_iso_3166_1_alpha_2_code: entity::country_iso_of(&ancestors, own_meta),
post_code: sources.post_code_of(record.id, path),
},
house_number: number.map(resolved_number::reported),
id: entity::path_id(record.id, path),
})
}