Good stuff

This commit is contained in:
Andras Schmelczer 2026-02-22 22:36:40 +00:00
parent 9da2db707f
commit 8032011708
32 changed files with 1052 additions and 374 deletions

View file

@ -66,24 +66,41 @@ impl LruCache {
}
}
/// Strip a numeric prefix like "000000-" from a filename stem.
/// "000000-bank-tube-station" → "bank-tube-station"
fn strip_numeric_prefix(stem: &str) -> &str {
if let Some(pos) = stem.find('-') {
if stem[..pos].chars().all(|ch| ch.is_ascii_digit()) {
return &stem[pos + 1..];
}
}
stem
}
/// Manages on-demand loading and caching of precomputed travel time parquet files.
///
/// Directory structure: `{base_dir}/{mode}/{slug}.parquet`
/// Directory structure: `{base_dir}/{mode}/{NNNNNN-slug}.parquet`
/// Files have a numeric prefix for uniqueness; lookups use the stripped slug.
/// Each parquet file has columns: `pcds` (String), `travel_minutes` (Int16).
pub struct TravelTimeStore {
base_dir: PathBuf,
/// Available transport modes (subdirectory names, e.g., "bicycle")
pub available_modes: Vec<String>,
/// mode → set of destination slugs (filenames without .parquet)
/// mode → set of destination slugs (numeric prefix stripped)
pub destinations: FxHashMap<String, FxHashSet<String>>,
/// (mode, stripped_slug) → full filename stem (with numeric prefix)
slug_to_file: FxHashMap<(String, String), String>,
cache: Mutex<LruCache>,
}
impl TravelTimeStore {
/// Scan the travel-times directory to discover available modes and destinations.
/// Filename stems have a numeric prefix (e.g., "000000-bank-tube-station") which
/// is stripped for slug lookups but preserved for file loading.
pub fn load(base_dir: &Path, cache_capacity: usize) -> anyhow::Result<Self> {
let mut available_modes = Vec::new();
let mut destinations: FxHashMap<String, FxHashSet<String>> = FxHashMap::default();
let mut slug_to_file: FxHashMap<(String, String), String> = FxHashMap::default();
for entry in std::fs::read_dir(base_dir)
.with_context(|| format!("Failed to read travel-times dir: {}", base_dir.display()))?
@ -103,7 +120,12 @@ impl TravelTimeStore {
let file_name = file_entry.file_name();
let file_name = file_name.to_string_lossy();
if file_name.ends_with(".parquet") {
let slug = file_name.trim_end_matches(".parquet").to_string();
let file_stem = file_name.trim_end_matches(".parquet");
let slug = strip_numeric_prefix(file_stem).to_string();
slug_to_file.insert(
(mode.clone(), slug.clone()),
file_stem.to_string(),
);
slugs.insert(slug);
}
}
@ -125,6 +147,7 @@ impl TravelTimeStore {
base_dir: base_dir.to_path_buf(),
available_modes,
destinations,
slug_to_file,
cache: Mutex::new(LruCache::new(cache_capacity)),
})
}
@ -142,11 +165,16 @@ impl TravelTimeStore {
}
}
// Load from file (no lock held — harmless if two threads load the same file)
// Resolve slug to actual filename (may have numeric prefix)
let file_stem = self
.slug_to_file
.get(&key)
.map(|val| val.as_str())
.unwrap_or(slug);
let path = self
.base_dir
.join(mode)
.join(format!("{}.parquet", slug));
.join(format!("{}.parquet", file_stem));
if !path.exists() {
bail!("Travel time file not found: {}", path.display());
}
@ -233,18 +261,15 @@ mod tests {
#[test]
fn slugify_basic() {
assert_eq!(slugify("Abbey Hey"), "abbey-hey");
assert_eq!(slugify("Abbots Bickington"), "abbots-bickington");
assert_eq!(slugify("London"), "london");
}
#[test]
fn slugify_special_chars() {
assert_eq!(slugify("A'Bhuaile Ghlas"), "a-bhuaile-ghlas");
}
#[test]
fn slugify_edges() {
assert_eq!(slugify(" Hello "), "hello");
assert_eq!(slugify("Abbey"), "abbey");
fn strip_numeric_prefix_basic() {
assert_eq!(strip_numeric_prefix("000000-bank-tube-station"), "bank-tube-station");
assert_eq!(strip_numeric_prefix("000123-abbey-hey"), "abbey-hey");
assert_eq!(strip_numeric_prefix("bank-tube-station"), "bank-tube-station");
assert_eq!(strip_numeric_prefix("london"), "london");
}
}

View file

@ -28,6 +28,10 @@ pub struct FeatureConfig {
pub raw: bool,
/// If true, the slider uses absolute min/max/step instead of percentile scaling
pub absolute: bool,
/// Listing modes this feature is available in (empty = all modes)
pub modes: &'static [&'static str],
/// Name of the linked feature that swaps when switching modes (empty = no link)
pub linked: &'static str,
}
/// Features whose histogram bins should be exactly 1 unit wide (one per integer).
@ -61,7 +65,7 @@ pub struct EnumFeatureGroup {
pub static FEATURE_GROUPS: &[FeatureGroup] = &[
FeatureGroup {
name: "Property",
name: "Properties in the area",
features: &[
FeatureConfig {
name: "Last known price",
@ -77,6 +81,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: true,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Estimated current price",
@ -92,6 +98,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: true,
modes: &["historical"],
linked: "Asking price",
},
FeatureConfig {
name: "Price per sqm",
@ -107,6 +115,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Est. price per sqm",
@ -122,6 +132,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Total floor area (sqm)",
@ -137,6 +149,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: " sqm",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Interior height (m)",
@ -152,6 +166,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: " m",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Number of bedrooms & living rooms",
@ -167,6 +183,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: " rooms",
raw: false,
absolute: true,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Estimated monthly rent",
@ -179,6 +197,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/mo",
raw: false,
absolute: false,
modes: &["historical"],
linked: "Asking rent (monthly)",
},
FeatureConfig {
name: "Date of last transaction",
@ -194,6 +214,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: true,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Construction age",
@ -209,6 +231,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: true,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Asking price",
@ -224,6 +248,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: true,
modes: &["buy"],
linked: "Estimated current price",
},
FeatureConfig {
name: "Asking rent (monthly)",
@ -239,6 +265,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/mo",
raw: false,
absolute: true,
modes: &["rent"],
linked: "Estimated monthly rent",
},
FeatureConfig {
name: "Bedrooms",
@ -254,6 +282,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: true,
modes: &["buy", "rent"],
linked: "",
},
FeatureConfig {
name: "Bathrooms",
@ -269,6 +299,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: true,
modes: &["buy", "rent"],
linked: "",
},
FeatureConfig {
name: "Listing date",
@ -284,6 +316,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: true,
absolute: false,
modes: &["buy", "rent"],
linked: "",
},
],
},
@ -304,6 +338,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: " mins",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Public transport to Fitzrovia (mins)",
@ -319,6 +355,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: " mins",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Cycling to Bank (mins)",
@ -334,6 +372,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: " mins",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Cycling to Fitzrovia (mins)",
@ -349,6 +389,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: " mins",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Number of public transport stations within 2km",
@ -364,6 +406,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
],
},
@ -384,6 +428,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Good+ primary schools within 5km",
@ -399,6 +445,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Good+ secondary schools within 5km",
@ -414,6 +462,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
],
},
@ -431,6 +481,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Employment Score (rate)",
@ -443,6 +495,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Health Deprivation and Disability Score",
@ -458,6 +512,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Living Environment Score",
@ -473,6 +529,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Indoors Sub-domain Score",
@ -488,6 +546,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Outdoors Sub-domain Score",
@ -503,6 +563,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
],
},
@ -524,6 +586,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Minor crime (avg/yr)",
@ -539,6 +603,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
],
},
@ -559,6 +625,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Violence and sexual offences (avg/yr)",
@ -574,6 +642,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Criminal damage and arson (avg/yr)",
@ -589,6 +659,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Burglary (avg/yr)",
@ -604,6 +676,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Vehicle crime (avg/yr)",
@ -619,6 +693,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Robbery (avg/yr)",
@ -634,6 +710,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Other theft (avg/yr)",
@ -649,6 +727,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Shoplifting (avg/yr)",
@ -664,6 +744,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Drugs (avg/yr)",
@ -679,6 +761,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Possession of weapons (avg/yr)",
@ -694,6 +778,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Public order (avg/yr)",
@ -709,6 +795,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Bicycle theft (avg/yr)",
@ -724,6 +812,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Theft from the person (avg/yr)",
@ -739,6 +829,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Other crime (avg/yr)",
@ -754,6 +846,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "/yr",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
],
},
@ -774,6 +868,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "%",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "% Asian",
@ -789,6 +885,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "%",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "% Black",
@ -804,6 +902,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "%",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "% Mixed",
@ -819,6 +919,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "%",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "% Other",
@ -834,6 +936,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "%",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
],
},
@ -854,6 +958,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Number of grocery shops and supermarkets within 2km",
@ -869,6 +975,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Number of parks within 2km",
@ -884,6 +992,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: "",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
],
},
@ -904,6 +1014,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: " dB",
raw: false,
absolute: false,
modes: &[],
linked: "",
},
FeatureConfig {
name: "Max available download speed (Mbps)",
@ -919,6 +1031,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
suffix: " Mbps",
raw: true,
absolute: false,
modes: &[],
linked: "",
},
],
},

View file

@ -16,6 +16,10 @@ fn is_false(val: &bool) -> bool {
!val
}
fn is_empty_slice(val: &&[&str]) -> bool {
val.is_empty()
}
#[derive(Clone, Serialize)]
#[serde(tag = "type")]
pub enum FeatureInfo {
@ -37,6 +41,10 @@ pub enum FeatureInfo {
raw: bool,
#[serde(skip_serializing_if = "is_false")]
absolute: bool,
#[serde(skip_serializing_if = "is_empty_slice")]
modes: &'static [&'static str],
#[serde(skip_serializing_if = "is_empty")]
linked: &'static str,
},
#[serde(rename = "enum")]
Enum {
@ -102,6 +110,8 @@ pub fn build_features_response(data: &PropertyData) -> FeaturesResponse {
suffix: feature_config.suffix,
raw: feature_config.raw,
absolute: feature_config.absolute,
modes: feature_config.modes,
linked: feature_config.linked,
});
}
}

View file

@ -138,16 +138,13 @@ pub async fn post_invites(
}
}
/// Validate an invite code. Requires authentication to prevent enumeration.
/// Validate an invite code. Public endpoint — codes are 12-char random alphanumeric
/// so enumeration is impractical, and the response only reveals valid/invalid + type.
pub async fn get_invite(
state: Arc<AppState>,
Extension(user): Extension<OptionalUser>,
Extension(_user): Extension<OptionalUser>,
Path(code): Path<String>,
) -> Response {
if user.0.is_none() {
return StatusCode::UNAUTHORIZED.into_response();
}
if let Err(msg) = validate_invite_code(&code) {
return (StatusCode::BAD_REQUEST, msg).into_response();
}