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

@ -6,6 +6,94 @@ use std::fs;
use std::path::Path;
use tracing::{debug, info};
use super::PlaceData;
/// Precomputed outcode data derived from postcode boundaries.
/// An outcode is the first part of a UK postcode (e.g. "E14" from "E14 2DG").
pub struct OutcodeData {
pub names: Vec<String>,
pub name_lower: Vec<String>,
pub centroids: Vec<(f32, f32)>,
pub cities: Vec<Option<String>>,
}
impl OutcodeData {
/// Derive outcode data by grouping postcodes by their outcode prefix and averaging centroids.
pub fn from_postcode_and_place_data(
postcode_data: &PostcodeData,
place_data: &PlaceData,
) -> Self {
// Group postcode centroids by outcode
let mut outcode_centroids: FxHashMap<String, Vec<(f32, f32)>> = FxHashMap::default();
for (idx, postcode) in postcode_data.postcodes.iter().enumerate() {
if let Some(space_idx) = postcode.find(' ') {
let outcode = &postcode[..space_idx];
outcode_centroids
.entry(outcode.to_string())
.or_default()
.push(postcode_data.centroids[idx]);
}
}
// Build sorted vecs
let mut entries: Vec<(String, (f32, f32))> = outcode_centroids
.into_iter()
.map(|(outcode, pts)| {
let count = pts.len() as f32;
let avg_lat = pts.iter().map(|(lat, _)| lat).sum::<f32>() / count;
let avg_lon = pts.iter().map(|(_, lon)| lon).sum::<f32>() / count;
(outcode, (avg_lat, avg_lon))
})
.collect();
entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
let names: Vec<String> = entries.iter().map(|(n, _)| n.clone()).collect();
let name_lower: Vec<String> = names.iter().map(|n| n.to_lowercase()).collect();
let centroids: Vec<(f32, f32)> = entries.iter().map(|(_, c)| *c).collect();
// Compute nearest city for each outcode (same algorithm as PlaceData)
let city_indices: Vec<usize> = place_data
.type_rank
.iter()
.enumerate()
.filter_map(|(idx, &rank)| if rank == 0 { Some(idx) } else { None })
.collect();
let cities: Vec<Option<String>> = centroids
.iter()
.map(|&(lat, lon)| {
let cos_lat = lat.to_radians().cos();
let mut best_dist_sq = f32::MAX;
let mut best_city: Option<&str> = None;
for &ci in &city_indices {
let dlat = place_data.lat[ci] - lat;
let dlon = (place_data.lon[ci] - lon) * cos_lat;
let dist_sq = dlat * dlat + dlon * dlon;
if dist_sq < best_dist_sq {
best_dist_sq = dist_sq;
best_city = Some(&place_data.name[ci]);
}
}
// ~100km threshold
if best_dist_sq < 0.81 {
best_city.map(|s| s.to_string())
} else {
None
}
})
.collect();
info!(outcodes = names.len(), "Outcode data derived from postcodes");
OutcodeData {
names,
name_lower,
centroids,
cities,
}
}
}
/// GeoJSON structures for parsing postcode boundary files
#[derive(Deserialize)]
struct FeatureCollection {