Support outcode & gps search

This commit is contained in:
Andras Schmelczer 2026-04-04 09:58:58 +01:00
parent 23d128ff63
commit 3853b5dce7
6 changed files with 188 additions and 3 deletions

View file

@ -52,6 +52,7 @@ pub async fn get_places(
let t0 = std::time::Instant::now();
let query_lower = query.to_lowercase();
let pd = &state.place_data;
let od = &state.outcode_data;
let tt_store = &state.travel_time_store;
// Linear scan — ~50-100k rows, <1ms
@ -99,7 +100,7 @@ pub async fn get_places(
matches.truncate(limit);
let results: Vec<PlaceResult> = matches
let mut results: Vec<PlaceResult> = matches
.iter()
.map(|(idx, .., slug)| PlaceResult {
name: pd.name[*idx].clone(),
@ -111,6 +112,49 @@ pub async fn get_places(
})
.collect();
// Also search outcodes (skip when mode filter is set — outcodes aren't travel destinations)
if mode_filter.is_none() {
let query_upper = query_lower.to_uppercase();
let mut outcode_results: Vec<PlaceResult> = od
.name_lower
.iter()
.enumerate()
.filter_map(|(idx, name)| {
if !name.starts_with(&query_lower) {
return None;
}
let is_exact = name.len() == query_lower.len();
Some((idx, is_exact))
})
.collect::<Vec<_>>()
.into_iter()
.map(|(idx, _is_exact)| PlaceResult {
name: od.names[idx].clone(),
slug: od.names[idx].to_lowercase(),
place_type: "outcode".to_string(),
lat: od.centroids[idx].0,
lon: od.centroids[idx].1,
city: od.cities[idx].clone(),
})
.collect();
// Sort outcodes: exact first, then by name length (shorter = broader area)
outcode_results.sort_unstable_by(|a, b| {
let a_exact = a.name.eq_ignore_ascii_case(&query_upper);
let b_exact = b.name.eq_ignore_ascii_case(&query_upper);
b_exact
.cmp(&a_exact)
.then(a.name.len().cmp(&b.name.len()))
});
// Prepend outcode results (up to 3) before place results, keeping total ≤ limit
outcode_results.truncate(3);
let place_slots = limit.saturating_sub(outcode_results.len());
results.truncate(place_slots);
outcode_results.append(&mut results);
results = outcode_results;
}
let elapsed = t0.elapsed();
info!(
query = query.as_str(),

View file

@ -28,6 +28,12 @@ pub struct PostcodesResponse {
features: Vec<Map<String, Value>>,
}
#[derive(Deserialize)]
pub struct NearestPostcodeParams {
lat: f64,
lng: f64,
}
#[derive(Deserialize)]
pub struct PostcodeParams {
bounds: Option<String>,
@ -311,6 +317,45 @@ pub async fn get_postcodes(
Ok(Json(response))
}
/// Find the nearest postcode to a given lat/lng coordinate.
pub async fn get_nearest_postcode(
State(shared): State<Arc<SharedState>>,
Query(params): Query<NearestPostcodeParams>,
) -> Result<Json<Value>, StatusCode> {
let state = shared.load_state();
let postcode_data = &state.postcode_data;
let query_lat = params.lat as f32;
let query_lng = params.lng as f32;
let cos_lat = (query_lat as f64).to_radians().cos() as f32;
let mut best_idx: Option<usize> = None;
let mut best_dist_sq = f32::MAX;
for (idx, &(pc_lat, pc_lon)) in postcode_data.centroids.iter().enumerate() {
let dlat = pc_lat - query_lat;
let dlon = (pc_lon - query_lng) * cos_lat;
let dist_sq = dlat * dlat + dlon * dlon;
if dist_sq < best_dist_sq {
best_dist_sq = dist_sq;
best_idx = Some(idx);
}
}
let idx = best_idx.ok_or(StatusCode::NOT_FOUND)?;
let (lat, lon) = postcode_data.centroids[idx];
let geometry = postcode_data.geometries[idx].clone();
let postcode = &postcode_data.postcodes[idx];
info!(postcode = %postcode, "GET /api/nearest-postcode");
Ok(Json(serde_json::json!({
"postcode": postcode,
"latitude": lat as f64,
"longitude": lon as f64,
"geometry": geometry,
})))
}
/// Look up a single postcode and return its centroid coordinates and geometry.
pub async fn get_postcode_lookup(
State(shared): State<Arc<SharedState>>,