use std::sync::Arc; use axum::extract::{Query, State}; use axum::http::StatusCode; use axum::response::Json; use rustc_hash::FxHashSet; use serde::{Deserialize, Serialize}; use tracing::info; use crate::data::slugify; use crate::state::SharedState; #[derive(Serialize)] pub struct DestinationResult { name: String, slug: String, place_type: String, #[serde(skip_serializing_if = "Option::is_none")] city: Option, } #[derive(Serialize)] pub struct DestinationsResponse { destinations: Vec, } #[derive(Deserialize)] pub struct DestinationsParams { mode: String, } pub async fn get_travel_destinations( State(shared): State>, Query(params): Query, ) -> Result, (StatusCode, String)> { let state = shared.load_state(); let mode = params.mode; let destinations = tokio::task::spawn_blocking(move || { let t0 = std::time::Instant::now(); let pd = &state.place_data; let tt_store = &state.travel_time_store; let slug_set = match tt_store.destinations.get(&mode) { Some(slugs) => slugs, None => return Vec::new(), }; // Find places that have travel time data for this mode let mut matches: Vec<(usize, String, u8, u32, usize)> = pd .name .iter() .enumerate() .filter_map(|(idx, name)| { let slug = slugify(name); if slug_set.contains(&slug) { Some((idx, slug, pd.type_rank[idx], pd.population[idx], name.len())) } else { None } }) .collect(); // Sort: type rank asc, population desc, name length asc matches.sort_unstable_by(|a, b| a.2.cmp(&b.2).then(b.3.cmp(&a.3)).then(a.4.cmp(&b.4))); // Deduplicate by slug — multiple places can share a name/slug // (e.g. "Richmond" as city + suburb), keep the best-ranked one let mut seen_slugs = FxHashSet::default(); matches.retain(|(_, slug, ..)| seen_slugs.insert(slug.clone())); let results: Vec = matches .into_iter() .map(|(idx, slug, ..)| DestinationResult { name: pd.name[idx].clone(), slug, place_type: pd.place_type.get(idx).to_string(), city: pd.city[idx].clone(), }) .collect(); let elapsed = t0.elapsed(); info!( mode = mode.as_str(), results = results.len(), ms = format_args!("{:.1}", elapsed.as_secs_f64() * 1000.0), "GET /api/travel-destinations" ); results }) .await .map_err(|error| (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()))?; Ok(Json(DestinationsResponse { destinations })) }