use std::cmp::Ordering;
use std::collections::BTreeMap;
use rusqlite::Connection;
use super::entity::{attach_wkt, database_status, query_match, query_output, query_service};
use super::filter::MAX_RESULTS;
use super::{base, coordinates, query_opts, text};
use crate::admin_level::country_index::country_index;
use crate::admin_level_hierarchy::search_index::{search_hit, search_tier};
use crate::admin_level_hierarchy::tantivy_index;
pub(super) fn by_text(bases: &mut [base], text: &str, opts: &query_opts) -> query_output {
let databases = databases_of(bases);
let mut indexed: Vec<&mut base> = bases.iter_mut().filter(|b| b.index.is_some()).collect();
let regions: Vec<Option<Vec<i64>>> = if opts.bounding.is_some() {
in_parallel(indexed.iter_mut().map(|b| &mut *b.conn).collect(), |conn| {
text::region_of(conn, opts)
})
} else {
indexed.iter().map(|_| None).collect()
};
let mut hits: Vec<Vec<search_hit>> = vec![];
for tier in search_tier::WALK {
let asked: Vec<(&tantivy_index, Option<&[i64]>)> = indexed
.iter()
.zip(®ions)
.map(|(b, region)| (b.index.expect("the base was kept for its index"), region.as_deref()))
.collect();
hits = in_parallel(asked, |(index, region)| text::hits_at(index, tier, text, opts, region));
if hits.iter().any(|h| !h.is_empty()) {
break;
}
}
let answering: Vec<(usize, &mut Connection, &[search_hit])> = indexed
.iter_mut()
.zip(&hits)
.enumerate()
.filter(|(_, (_, h))| !h.is_empty())
.map(|(i, (b, h))| (i, &mut *b.conn, h.as_slice()))
.collect();
let answered = answering.len();
let answers = in_parallel(answering, |(i, conn, hits)| {
(i, text::matches_of(conn, text, hits, opts))
});
let mut tagged = tag_by_base(answers.into_iter());
if answered > 1 {
tagged.sort_by(|(_, a), (_, b)| {
descending(b.similarity, a.similarity).then_with(|| descending(b.score, a.score))
});
tagged.truncate(MAX_RESULTS as usize);
}
query_output {
service: query_service::text_to_address,
databases,
matches: with_wkt(tagged, |i| &*indexed[i].conn, opts),
}
}
pub(super) fn by_coordinates(
bases: &mut [base],
latitude: f64,
longitude: f64,
opts: &query_opts,
) -> query_output {
let country = country_index::shared().country_at(longitude, latitude);
let holder = bases
.iter_mut()
.find(|b| country.is_some_and(|iso| b.iso.eq_ignore_ascii_case(iso)));
let Some(holder) = holder else {
return query_output {
service: query_service::coordinates_to_address,
databases: vec![],
matches: vec![],
};
};
let databases = vec![database_status::of(holder.file, holder.conn, holder.index)];
let matches = coordinates::run(holder.conn, latitude, longitude, opts);
let tagged = tag_by_base(std::iter::once((0, matches)));
query_output {
service: query_service::coordinates_to_address,
databases,
matches: with_wkt(tagged, |_| &*holder.conn, opts),
}
}
pub(crate) fn in_parallel<I: Send, T: Send>(items: Vec<I>, f: impl Fn(I) -> T + Sync) -> Vec<T> {
if items.len() < 2 {
return items.into_iter().map(f).collect();
}
std::thread::scope(|scope| {
let f = &f;
let handles: Vec<_> = items
.into_iter()
.map(|item| scope.spawn(move || f(item)))
.collect();
handles
.into_iter()
.map(|handle| handle.join().expect("a base answers or panics"))
.collect()
})
}
fn tag_by_base(
answers: impl Iterator<Item = (usize, Vec<query_match>)>,
) -> Vec<(usize, query_match)> {
answers
.flat_map(|(i, matches)| matches.into_iter().map(move |m| (i, m)))
.collect()
}
fn with_wkt<'a>(
mut tagged: Vec<(usize, query_match)>,
conn_of: impl Fn(usize) -> &'a Connection,
opts: &query_opts,
) -> Vec<query_match> {
if opts.include_wkt {
let mut by_base: BTreeMap<usize, Vec<&mut query_match>> = BTreeMap::new();
for (i, m) in &mut tagged {
by_base.entry(*i).or_default().push(m);
}
for (i, mut matches) in by_base {
attach_wkt(conn_of(i), &mut matches);
}
}
tagged.into_iter().map(|(_, m)| m).collect()
}
fn databases_of(bases: &[base]) -> Vec<database_status> {
bases
.iter()
.map(|b| database_status::of(b.file, b.conn, b.index))
.collect()
}
fn descending(a: Option<f32>, b: Option<f32>) -> Ordering {
a.partial_cmp(&b).unwrap_or(Ordering::Equal)
}