use geo::Centroid;
use rusqlite::Connection;
use std::collections::{HashMap, HashSet};
use crate::extract::admin_levels::osm_admin_level;
use crate::index::admin_levels_hierarchy_tantivy::{
build_entity_text, tantivy_index, tokenize,
};
fn search_hits(
tantivy_index: &tantivy_index,
query: &str,
last_admin_levels: Option<&[u8]>,
allowed_ids: Option<&[i64]>,
) -> Vec<(i64, f32)> {
tantivy_index.search(query, super::MAX_FTS_HITS as usize, last_admin_levels, allowed_ids)
}
fn doc_text(
own_name: &str,
own_post_code: Option<&str>,
ancestors: &[&crate::database::admin_levels::admin_meta_row],
) -> String {
let mut out = build_entity_text(own_name, own_post_code);
for a in ancestors {
out.push(' ');
out.push_str(&build_entity_text(&a.name, a.post_code.as_deref()));
}
out
}
fn token_coverage(query_tokens: &[String], doc_text: &str) -> f32 {
if query_tokens.is_empty() {
return 0.0;
}
let doc_tokens: HashSet<String> = tokenize(doc_text).into_iter().collect();
let hits = query_tokens
.iter()
.filter(|t| doc_tokens.contains(t.as_str()))
.count();
hits as f32 / query_tokens.len() as f32
}
fn build_match(
record: &crate::database::admin_levels::admin_area_row,
hierarchy: Option<&crate::database::admin_levels_hierarchy::hierarchy_lookup_row>,
meta_map: &HashMap<i64, crate::database::admin_levels::admin_meta_row>,
query_tokens: &[String],
raw_score: f32,
friendly_name_format: Option<&str>,
wkt_by_id: &HashMap<i64, String>,
) -> Option<super::query_match> {
let geom = record.wkb.as_ref()?.geometry();
let centroid = geom.centroid()?;
let ancestor_ids = hierarchy.map(|h| h.ancestor_ids.as_slice()).unwrap_or(&[]);
let mut ancestors: Vec<&crate::database::admin_levels::admin_meta_row> = ancestor_ids
.iter()
.filter_map(|id| meta_map.get(id))
.collect();
ancestors.sort_by_key(|a| a.admin_level);
let mut admin_levels: Vec<super::admin_level> = ancestors
.iter()
.map(|a| super::admin_level {
level: a.admin_level,
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: record.admin_level,
name: record.name.clone(),
osm_relation_id: record.relation_id,
osm_way_id: record.way_id,
wkt: wkt_by_id.get(&record.id).cloned(),
});
let friendly_name = match friendly_name_format {
Some(fmt) => super::render_friendly_name(fmt, &admin_levels),
None => hierarchy
.map(|h| h.user_friendly_name.clone())
.unwrap_or_else(|| record.name.clone()),
};
let country_iso = ancestors
.iter()
.copied()
.chain(meta_map.get(&record.id))
.find(|a| a.admin_level == osm_admin_level::country as u8)
.and_then(|a| a.country_iso_code.clone());
let post_code = ancestors
.iter()
.filter(|a| a.post_code.is_some())
.max_by_key(|a| a.admin_level)
.and_then(|a| a.post_code.clone());
let is_street = record.admin_level == osm_admin_level::street as u8;
let admin_level_id = if is_street { Some(record.id) } else { None };
let record_post_code = meta_map
.get(&record.id)
.and_then(|m| m.post_code.as_deref());
let doc_text = doc_text(&record.name, record_post_code, &ancestors);
let coverage = token_coverage(query_tokens, &doc_text);
Some(super::query_match {
admin_levels,
latitude: super::round5(centroid.y()),
longitude: super::round5(centroid.x()),
coordinates_distance_in_meters: None,
similarity: Some(super::round5(coverage as f64) as f32),
score: Some(raw_score),
friendly_name,
attributes: super::query_match_attributes {
country_iso_3166_1_alpha_2_code: country_iso,
post_code,
},
house_number: None,
id: record.id as u64,
admin_level_id,
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn run(
conn: &Connection,
tantivy_index: &tantivy_index,
query: &str,
friendly_name_format: Option<&str>,
last_admin_levels: Option<&[u8]>,
bounding_wkt: Option<&super::bounding_geometry>,
min_quality: Option<f64>,
include_wkt: bool,
) -> super::query_output {
let region_ids =
bounding_wkt.map(|b| crate::database::admin_levels::ids_in_bounding_box(conn, b.envelope));
if region_ids.as_ref().is_some_and(|r| r.is_empty()) {
return super::query_output {
service: super::query_service::text_to_address,
matches: vec![],
};
}
let hits = search_hits(tantivy_index, query, last_admin_levels, region_ids.as_deref());
if hits.is_empty() {
return super::query_output {
service: super::query_service::text_to_address,
matches: vec![],
};
}
let query_tokens = tokenize(query);
let ids: Vec<i64> = hits.iter().map(|(id, _)| *id).collect();
let scores: HashMap<i64, f32> = hits.into_iter().collect();
let hierarchies = crate::database::admin_levels_hierarchy::load_by_ids(conn, &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 = ids.clone();
meta_ids.extend(all_ancestor_ids);
meta_ids.sort_unstable();
meta_ids.dedup();
let meta_map = crate::database::admin_levels::load_metadata_by_ids(conn, &meta_ids);
let wkt_by_id = super::load_wkt_by_ids(conn, &meta_ids, include_wkt);
let records = crate::database::admin_levels::load_full_by_ids(conn, &ids);
let record_map: HashMap<i64, &crate::database::admin_levels::admin_area_row> =
records.iter().map(|r| (r.id, r)).collect();
let mut matches: Vec<super::query_match> = ids
.iter()
.filter_map(|id| {
let record = record_map.get(id)?;
let hierarchy = hierarchies.get(id);
let raw_score = *scores.get(id).unwrap_or(&0.0);
build_match(
record,
hierarchy,
&meta_map,
&query_tokens,
raw_score,
friendly_name_format,
&wkt_by_id,
)
})
.collect();
super::house_number::enrich_house_number_from_query(conn, query, &mut matches, friendly_name_format);
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)
})
});
super::apply_filters_and_truncate(&mut matches, min_quality, bounding_wkt, last_admin_levels);
super::query_output {
service: super::query_service::text_to_address,
matches,
}
}
#[cfg(test)]
#[path = "text_search.test.rs"]
mod text_search_test;