1077 lines
49 KiB
Rust
1077 lines
49 KiB
Rust
//! Static feature configuration. Every numeric and enum column in wide.parquet
|
||
//! must be declared here. Unknown columns cause a startup panic.
|
||
|
||
pub enum Bounds {
|
||
/// Fixed min/max values for the slider
|
||
Fixed { min: f32, max: f32 },
|
||
/// Compute percentile from data at startup
|
||
Percentile { low: f64, high: f64 },
|
||
}
|
||
|
||
pub struct FeatureConfig {
|
||
/// Must match parquet column name exactly (also used as display label)
|
||
pub name: &'static str,
|
||
pub bounds: Bounds,
|
||
/// Slider step size. Controls the granularity of the range slider in the UI.
|
||
pub step: f32,
|
||
/// Short one-line description shown in the filter sidebar
|
||
pub description: &'static str,
|
||
/// Longer description explaining methodology, data source, and caveats
|
||
pub detail: &'static str,
|
||
/// Data source slug for linking to /learn#<slug>
|
||
pub source: &'static str,
|
||
/// Display prefix (e.g. "£")
|
||
pub prefix: &'static str,
|
||
/// Display suffix (e.g. " mins")
|
||
pub suffix: &'static str,
|
||
/// If true, show full integer (no k/M abbreviation)
|
||
pub raw: bool,
|
||
/// If true, the slider uses absolute min/max/step instead of percentile scaling
|
||
pub absolute: bool,
|
||
}
|
||
|
||
/// Features whose histogram bins should be exactly 1 unit wide (one per integer).
|
||
/// p1/p99 are snapped to integer boundaries before binning.
|
||
pub const INTEGER_BIN_FEATURES: &[&str] =
|
||
&["Number of bedrooms & living rooms", "Bedrooms", "Bathrooms"];
|
||
|
||
pub struct FeatureGroup {
|
||
pub name: &'static str,
|
||
pub features: &'static [FeatureConfig],
|
||
}
|
||
|
||
pub struct EnumFeatureConfig {
|
||
pub name: &'static str,
|
||
/// If set, values are presented in this order instead of alphabetical.
|
||
/// Values not listed are appended alphabetically after the ordered ones.
|
||
pub order: Option<&'static [&'static str]>,
|
||
/// Short one-line description shown in the filter sidebar
|
||
pub description: &'static str,
|
||
/// Longer description explaining methodology, data source, and caveats
|
||
pub detail: &'static str,
|
||
/// Data source slug for linking to /learn#<slug>
|
||
pub source: &'static str,
|
||
}
|
||
|
||
pub struct EnumFeatureGroup {
|
||
pub name: &'static str,
|
||
pub features: &'static [EnumFeatureConfig],
|
||
}
|
||
|
||
|
||
pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
||
FeatureGroup {
|
||
name: "Property",
|
||
features: &[
|
||
FeatureConfig {
|
||
name: "Last known price",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 2_000_000.0,
|
||
},
|
||
step: 10000.0,
|
||
description: "Most recent sale price from the Land Registry",
|
||
detail: "The last recorded sale price for this property from HM Land Registry Price Paid data. Covers residential sales in England and Wales. May be years old if the property hasn't sold recently.",
|
||
source: "price-paid",
|
||
prefix: "£",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: true,
|
||
},
|
||
FeatureConfig {
|
||
name: "Estimated current price",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 2_000_000.0,
|
||
},
|
||
step: 10000.0,
|
||
description: "Inflation-adjusted estimate of the current property value",
|
||
detail: "Estimated by applying a repeat-sales price index to the last known sale price, plus a renovation premium for properties with post-sale improvements detected from EPC records (extensions, renovations, remodeling). The index tracks price changes within each postcode sector and property type. Renovation premiums are estimated per area from observed repeat-sale pairs and decay over time. Properties sold recently will have estimates close to their sale price; older sales are adjusted more.",
|
||
source: "price-paid",
|
||
prefix: "£",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: true,
|
||
},
|
||
FeatureConfig {
|
||
name: "Price per sqm",
|
||
bounds: Bounds::Percentile {
|
||
low: 0.0,
|
||
high: 98.0,
|
||
},
|
||
step: 100.0,
|
||
description: "Sale price divided by total floor area",
|
||
detail: "Calculated by dividing the last known sale price by the total floor area from the EPC certificate. Useful for comparing value across different-sized properties. Only available where both price and floor area data exist.",
|
||
source: "price-paid",
|
||
prefix: "£",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Est. price per sqm",
|
||
bounds: Bounds::Percentile {
|
||
low: 0.0,
|
||
high: 98.0,
|
||
},
|
||
step: 100.0,
|
||
description: "Estimated current price divided by total floor area",
|
||
detail: "Calculated by dividing the inflation-adjusted estimated current price (including any renovation premium) by the total floor area from the EPC certificate. Provides a more up-to-date price-per-area comparison than the historical sale price per sqm.",
|
||
source: "price-paid",
|
||
prefix: "£",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Total floor area (sqm)",
|
||
bounds: Bounds::Percentile {
|
||
low: 0.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Internal floor area from the EPC survey",
|
||
detail: "Total useful floor area in square metres as measured during the Energy Performance Certificate assessment. Includes all habitable rooms but excludes garages, outbuildings, and external areas.",
|
||
source: "epc",
|
||
prefix: "",
|
||
suffix: " sqm",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Interior height (m)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 0.1,
|
||
description: "Average storey height from the EPC survey",
|
||
detail: "Average internal floor-to-ceiling height in metres as recorded during the Energy Performance Certificate assessment. Calculated by dividing the total internal volume by the total floor area.",
|
||
source: "epc",
|
||
prefix: "",
|
||
suffix: " m",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Number of bedrooms & living rooms",
|
||
bounds: Bounds::Fixed {
|
||
min: 1.0,
|
||
max: 12.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Count of habitable rooms from the EPC survey",
|
||
detail: "Total number of habitable rooms (bedrooms plus living rooms) as recorded in the Energy Performance Certificate. Kitchens and bathrooms are typically excluded unless they are large enough to count as habitable rooms.",
|
||
source: "epc",
|
||
prefix: "",
|
||
suffix: " rooms",
|
||
raw: false,
|
||
absolute: true,
|
||
},
|
||
FeatureConfig {
|
||
name: "Estimated monthly rent",
|
||
bounds: Bounds::Percentile { low: 2.0, high: 98.0 },
|
||
step: 25.0,
|
||
description: "Median monthly private rent for the local area and bedroom count",
|
||
detail: "Median monthly rental price from ONS Private Rental Market Summary Statistics (Oct 2022 - Sep 2023). Matched by local authority district and estimated bedroom count (habitable rooms minus 1). Based on Valuation Office Agency lettings data.",
|
||
source: "ons-rental",
|
||
prefix: "£",
|
||
suffix: "/mo",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Date of last transaction",
|
||
bounds: Bounds::Fixed {
|
||
min: 1995.0,
|
||
max: 2026.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Date of the most recent sale from the Land Registry",
|
||
detail: "The date of the most recent recorded sale for this property from HM Land Registry Price Paid data. Stored as a datetime in the data; converted to fractional year for filtering and charting.",
|
||
source: "price-paid",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: true,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Construction age",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 2026.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Estimated year of construction from the EPC",
|
||
detail: "The approximate year of construction as recorded in the Energy Performance Certificate. Derived from the construction age band (e.g. '1930-1949') by taking the midpoint. May be approximate, especially for older buildings.",
|
||
source: "epc",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: true,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Asking price",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 2_000_000.0,
|
||
},
|
||
step: 10000.0,
|
||
description: "Listed asking price for properties currently for sale",
|
||
detail: "The advertised asking price for properties currently listed for sale on online property portals. Only populated for 'For sale' listings; null for historical sales and rentals.",
|
||
source: "online-listings",
|
||
prefix: "£",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: true,
|
||
},
|
||
FeatureConfig {
|
||
name: "Asking rent (monthly)",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 10_000.0,
|
||
},
|
||
step: 50.0,
|
||
description: "Listed monthly rent for properties currently for rent",
|
||
detail: "The advertised rental price normalized to monthly for properties currently listed for rent on online property portals. Weekly rents are converted (×52/12), yearly (/12), daily (×365.25/12), and quarterly (/3). Only populated for 'For rent' listings.",
|
||
source: "online-listings",
|
||
prefix: "£",
|
||
suffix: "/mo",
|
||
raw: false,
|
||
absolute: true,
|
||
},
|
||
FeatureConfig {
|
||
name: "Bedrooms",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 10.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Number of bedrooms from online listing",
|
||
detail: "Number of bedrooms as advertised in the online property listing. Only populated for online listings (for sale and for rent); null for historical sales.",
|
||
source: "online-listings",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: true,
|
||
},
|
||
FeatureConfig {
|
||
name: "Bathrooms",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 10.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Number of bathrooms from online listing",
|
||
detail: "Number of bathrooms as advertised in the online property listing. Only populated for online listings (for sale and for rent); null for historical sales.",
|
||
source: "online-listings",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: true,
|
||
},
|
||
FeatureConfig {
|
||
name: "Listing date",
|
||
bounds: Bounds::Fixed {
|
||
min: 2006.0,
|
||
max: 2026.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Date the property was first listed online",
|
||
detail: "The date when the property listing first appeared on the online property portal. Stored as a datetime; converted to fractional year for filtering. Only populated for online listings.",
|
||
source: "online-listings",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: true,
|
||
absolute: false,
|
||
},
|
||
],
|
||
},
|
||
FeatureGroup {
|
||
name: "Transport",
|
||
features: &[
|
||
FeatureConfig {
|
||
name: "Public transport to Bank (mins)",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 180.0,
|
||
},
|
||
step: 2.0,
|
||
description: "Public transport journey time to Bank station",
|
||
detail: "Journey time in minutes by public transport to Bank station in the City of London, using TfL's Journey Planner API. Calculated for weekday morning commute times.",
|
||
source: "tfl-journey-times",
|
||
prefix: "",
|
||
suffix: " mins",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Public transport to Fitzrovia (mins)",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 180.0,
|
||
},
|
||
step: 2.0,
|
||
description: "Public transport journey time to Fitzrovia",
|
||
detail: "Journey time in minutes by public transport to Fitzrovia in central London, using TfL's Journey Planner API. Calculated for weekday morning commute times.",
|
||
source: "tfl-journey-times",
|
||
prefix: "",
|
||
suffix: " mins",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Cycling to Bank (mins)",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 180.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Cycling time to Bank station",
|
||
detail: "Cycling journey time in minutes to Bank station, as calculated by the TfL Journey Planner API. Uses TfL's default cycling speed and route preferences.",
|
||
source: "tfl-journey-times",
|
||
prefix: "",
|
||
suffix: " mins",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Cycling to Fitzrovia (mins)",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 180.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Cycling time to Fitzrovia",
|
||
detail: "Cycling journey time in minutes to Fitzrovia, as calculated by the TfL Journey Planner API. Uses TfL's default cycling speed and route preferences.",
|
||
source: "tfl-journey-times",
|
||
prefix: "",
|
||
suffix: " mins",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Number of public transport stations within 2km",
|
||
bounds: Bounds::Percentile {
|
||
low: 5.0,
|
||
high: 95.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Number of public transport stops within 2km",
|
||
detail: "Count of bus stops, rail stations, tube stations, tram stops, and other public transport access points within a 2km radius of the property's postcode. Derived from the NaPTAN (National Public Transport Access Nodes) dataset.",
|
||
source: "naptan",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
],
|
||
},
|
||
FeatureGroup {
|
||
name: "Education",
|
||
features: &[
|
||
FeatureConfig {
|
||
name: "Education, Skills and Training Score",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 0.1,
|
||
description: "IoD education score for the local area (higher = better)",
|
||
detail: "From the English Indices of Deprivation (inverted so higher = better). Measures education, skills and training quality in the local area (LSOA). Higher scores indicate less deprivation. Combines children/young people sub-domain (school attainment, entry to higher education) and adult skills sub-domain (adult qualifications, English language proficiency).",
|
||
source: "iod",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Good+ primary schools within 5km",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 30.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Primary schools rated Good or Outstanding by Ofsted nearby",
|
||
detail: "Number of state-funded primary schools within 5km that have a current Ofsted rating of Good or Outstanding. Based on the latest inspection outcomes dataset. Schools that have not yet been inspected are excluded.",
|
||
source: "ofsted",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Good+ secondary schools within 5km",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 15.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Secondary schools rated Good or Outstanding by Ofsted nearby",
|
||
detail: "Number of state-funded secondary schools within 5km that have a current Ofsted rating of Good or Outstanding. Based on the latest inspection outcomes dataset. Schools that have not yet been inspected are excluded.",
|
||
source: "ofsted",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
],
|
||
},
|
||
FeatureGroup {
|
||
name: "Deprivation",
|
||
features: &[
|
||
FeatureConfig {
|
||
name: "Income Score (rate)",
|
||
bounds: Bounds::Fixed { min: 0.0, max: 0.6 },
|
||
step: 0.01,
|
||
description: "Income deprivation rate, inverted (higher = less deprived)",
|
||
detail: "From the English Indices of Deprivation (inverted so higher = better). Higher values indicate less income deprivation. Based on Income Support, income-based Jobseeker's Allowance, income-based Employment and Support Allowance, Pension Credit, Working Tax Credit and Child Tax Credit, Universal Credit, and asylum seekers.",
|
||
source: "iod",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Employment Score (rate)",
|
||
bounds: Bounds::Fixed { min: 0.0, max: 0.4 },
|
||
step: 0.01,
|
||
description: "Employment deprivation rate, inverted (higher = less deprived)",
|
||
detail: "From the English Indices of Deprivation (inverted so higher = better). Higher values indicate less employment deprivation. Based on claimants of Jobseeker's Allowance, Employment and Support Allowance, Incapacity Benefit, Severe Disablement Allowance, Carer's Allowance, and relevant Universal Credit claimants.",
|
||
source: "iod",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Health Deprivation and Disability Score",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 0.1,
|
||
description: "Health and disability score (higher = better health outcomes)",
|
||
detail: "From the English Indices of Deprivation (inverted so higher = better). Higher scores indicate lower risk of premature death and better quality of life. Derived from years of potential life lost, comparative illness and disability ratio, acute morbidity, and mood and anxiety disorders.",
|
||
source: "iod",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Living Environment Score",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 0.1,
|
||
description: "Quality of the local indoor and outdoor environment (higher = better)",
|
||
detail: "From the English Indices of Deprivation (inverted so higher = better). Measures the quality of the local environment. Combines the Indoors sub-domain (housing quality, central heating, housing conditions) and Outdoors sub-domain (air quality, road traffic accidents). Higher scores indicate better living environments.",
|
||
source: "iod",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Indoors Sub-domain Score",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 0.1,
|
||
description: "Housing quality and conditions (higher = better)",
|
||
detail: "From the English Indices of Deprivation, Living Environment domain (inverted so higher = better). Measures the quality of housing stock: central heating availability, housing condition, and Decent Homes standards. Higher scores indicate better housing conditions.",
|
||
source: "iod",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Outdoors Sub-domain Score",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 0.1,
|
||
description: "Air quality and road safety (higher = better)",
|
||
detail: "From the English Indices of Deprivation, Living Environment domain (inverted so higher = better). Measures the outdoor living environment quality through air quality indicators and road traffic accident casualties involving pedestrians and cyclists. Higher scores indicate better outdoor environments.",
|
||
source: "iod",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
],
|
||
},
|
||
|
||
FeatureGroup {
|
||
name: "Crime summary",
|
||
features: &[
|
||
FeatureConfig {
|
||
name: "Serious crime (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Aggregate of serious crime categories per year",
|
||
detail: "Sum of violence, robbery, burglary, and weapons possession per year in the LSOA, from police.uk street-level crime data (2023-2025). Provides a single serious crime metric.",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Minor crime (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Aggregate of minor crime categories per year",
|
||
detail: "Sum of anti-social behaviour, shoplifting, bicycle theft, and other lower-severity crime per year in the LSOA, from police.uk street-level crime data (2023-2025). Provides a single minor crime metric.",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
],
|
||
},
|
||
FeatureGroup {
|
||
name: "Crime",
|
||
features: &[
|
||
FeatureConfig {
|
||
name: "Anti-social behaviour (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Average yearly anti-social behaviour incidents in the area",
|
||
detail: "Average number of anti-social behaviour incidents per year in the LSOA, from police.uk street-level crime data (2023-2025). Includes nuisance, environmental, and personal anti-social behaviour.",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Violence and sexual offences (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Average yearly violent and sexual offences in the area",
|
||
detail: "Average number of violence and sexual offences per year in the LSOA, from police.uk street-level crime data (2023-2025). Includes assault, harassment, and sexual offences.",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Criminal damage and arson (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Average yearly criminal damage and arson in the area",
|
||
detail: "Average number of criminal damage and arson incidents per year in the LSOA, from police.uk street-level crime data (2023-2025).",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Burglary (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Average yearly burglary offences in the area",
|
||
detail: "Average number of burglary offences per year in the LSOA, from police.uk street-level crime data (2023-2025). Includes residential and commercial burglary.",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Vehicle crime (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Average yearly vehicle crime in the area",
|
||
detail: "Average number of vehicle crime incidents per year in the LSOA, from police.uk street-level crime data (2023-2025). Includes theft of and from vehicles.",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Robbery (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Average yearly robbery offences in the area",
|
||
detail: "Average number of robbery offences per year in the LSOA, from police.uk street-level crime data (2023-2025). Robbery involves theft with force or threat of force.",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Other theft (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Average yearly other theft offences in the area",
|
||
detail: "Average number of 'other theft' offences per year in the LSOA, from police.uk street-level crime data (2023-2025). Includes theft not classified under burglary, vehicle crime, shoplifting, or bicycle theft.",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Shoplifting (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Average yearly shoplifting offences in the area",
|
||
detail: "Average number of shoplifting offences per year in the LSOA, from police.uk street-level crime data (2023-2025).",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Drugs (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Average yearly drug offences in the area",
|
||
detail: "Average number of drug offences per year in the LSOA, from police.uk street-level crime data (2023-2025). Includes possession and trafficking offences.",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Possession of weapons (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Average yearly weapons possession offences in the area",
|
||
detail: "Average number of possession of weapons offences per year in the LSOA, from police.uk street-level crime data (2023-2025).",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Public order (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Average yearly public order offences in the area",
|
||
detail: "Average number of public order offences per year in the LSOA, from police.uk street-level crime data (2023-2025). Includes causing fear, alarm, or distress.",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Bicycle theft (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Average yearly bicycle theft in the area",
|
||
detail: "Average number of bicycle theft offences per year in the LSOA, from police.uk street-level crime data (2023-2025).",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Theft from the person (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Average yearly theft from the person in the area",
|
||
detail: "Average number of theft from the person offences per year in the LSOA, from police.uk street-level crime data (2023-2025). Includes pickpocketing and bag snatching without force.",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Other crime (avg/yr)",
|
||
bounds: Bounds::Percentile {
|
||
low: 2.0,
|
||
high: 98.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Average yearly other crime in the area",
|
||
detail: "Average number of other crime offences per year in the LSOA, from police.uk street-level crime data (2023-2025). A catch-all category for offences not classified elsewhere.",
|
||
source: "crime",
|
||
prefix: "",
|
||
suffix: "/yr",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
],
|
||
},
|
||
FeatureGroup {
|
||
name: "Demographics",
|
||
features: &[
|
||
FeatureConfig {
|
||
name: "% White",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 100.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Percentage of population identifying as White",
|
||
detail: "From the 2021 Census. Percentage of the local authority population identifying as White (English, Welsh, Scottish, Northern Irish, British, Irish, Gypsy or Irish Traveller, Roma, or any other White background).",
|
||
source: "ethnicity",
|
||
prefix: "",
|
||
suffix: "%",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "% Asian",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 100.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Percentage of population identifying as Asian",
|
||
detail: "From the 2021 Census. Percentage of the local authority population identifying as Asian or Asian British (Indian, Pakistani, Bangladeshi, Chinese, or any other Asian background).",
|
||
source: "ethnicity",
|
||
prefix: "",
|
||
suffix: "%",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "% Black",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 100.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Percentage of population identifying as Black",
|
||
detail: "From the 2021 Census. Percentage of the local authority population identifying as Black, Black British, Caribbean, or African.",
|
||
source: "ethnicity",
|
||
prefix: "",
|
||
suffix: "%",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "% Mixed",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 100.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Percentage of population identifying as Mixed or Multiple ethnic groups",
|
||
detail: "From the 2021 Census. Percentage of the local authority population identifying as Mixed or Multiple ethnic groups (White and Black Caribbean, White and Black African, White and Asian, or any other Mixed or Multiple background).",
|
||
source: "ethnicity",
|
||
prefix: "",
|
||
suffix: "%",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "% Other",
|
||
bounds: Bounds::Fixed {
|
||
min: 0.0,
|
||
max: 100.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Percentage of population identifying as Other ethnic group",
|
||
detail: "From the 2021 Census. Percentage of the local authority population identifying as Other ethnic group (Arab or any other ethnic group not covered by the main categories).",
|
||
source: "ethnicity",
|
||
prefix: "",
|
||
suffix: "%",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
],
|
||
},
|
||
FeatureGroup {
|
||
name: "Amenities",
|
||
features: &[
|
||
FeatureConfig {
|
||
name: "Number of restaurants within 2km",
|
||
bounds: Bounds::Percentile {
|
||
low: 5.0,
|
||
high: 95.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Number of restaurants and cafes within 2km",
|
||
detail: "Count of restaurants, cafes, and food establishments within a 2km radius of the property's postcode centroid. Derived from OpenStreetMap POI data using haversine distance calculation with a 0.05° spatial grid for candidate reduction.",
|
||
source: "osm-pois",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Number of grocery shops and supermarkets within 2km",
|
||
bounds: Bounds::Percentile {
|
||
low: 5.0,
|
||
high: 95.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Number of grocery shops and supermarkets within 2km",
|
||
detail: "Count of supermarkets, convenience stores, and other grocery shops within a 2km radius of the property's postcode centroid. Derived from OpenStreetMap POI data.",
|
||
source: "osm-pois",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Number of parks within 2km",
|
||
bounds: Bounds::Percentile {
|
||
low: 5.0,
|
||
high: 95.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Number of parks and green spaces within 2km",
|
||
detail: "Count of parks, gardens, nature reserves, and other green spaces within a 2km radius of the property's postcode centroid. Derived from OpenStreetMap POI data.",
|
||
source: "osm-pois",
|
||
prefix: "",
|
||
suffix: "",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
],
|
||
},
|
||
FeatureGroup {
|
||
name: "Environment",
|
||
features: &[
|
||
FeatureConfig {
|
||
name: "Noise (dB)",
|
||
bounds: Bounds::Fixed {
|
||
min: 50.0,
|
||
max: 80.0,
|
||
},
|
||
step: 1.0,
|
||
description: "Road noise level at the postcode in decibels (Lden)",
|
||
detail: "Road noise level in decibels (Lden — day-evening-night 24-hour weighted average) from Defra's Strategic Noise Mapping Round 4 (2022). Modelled at 4m above ground on a 10m grid. Sampled at postcode centroids via WCS GeoTIFF tiles. Values above ~55 dB are generally considered noticeable; above ~70 dB can affect health.",
|
||
source: "noise",
|
||
prefix: "",
|
||
suffix: " dB",
|
||
raw: false,
|
||
absolute: false,
|
||
},
|
||
FeatureConfig {
|
||
name: "Max available download speed (Mbps)",
|
||
bounds: Bounds::Percentile {
|
||
low: 5.0,
|
||
high: 95.0,
|
||
},
|
||
step: 10.0,
|
||
description: "Maximum broadband download speed available at the postcode",
|
||
detail: "Maximum available fixed broadband download speed in Megabits per second, from Ofcom's Connected Nations 2025 report. Measured at Output Area level and represents the maximum speed available from any provider, not actual achieved speeds.",
|
||
source: "broadband",
|
||
prefix: "",
|
||
suffix: " Mbps",
|
||
raw: true,
|
||
absolute: false,
|
||
},
|
||
],
|
||
},
|
||
];
|
||
|
||
pub static ENUM_FEATURE_GROUPS: &[EnumFeatureGroup] = &[
|
||
EnumFeatureGroup {
|
||
name: "Property",
|
||
features: &[
|
||
EnumFeatureConfig {
|
||
name: "Listing status",
|
||
order: Some(&["Historical sale", "For sale", "For rent"]),
|
||
description: "Whether the property is from historical sales, currently for sale, or for rent",
|
||
detail: "Indicates the source of the property record: 'Historical sale' from HM Land Registry Price Paid data, 'For sale' from current online buy listings, or 'For rent' from current online rental listings.",
|
||
source: "online-listings",
|
||
},
|
||
EnumFeatureConfig {
|
||
name: "Leashold/Freehold",
|
||
order: Some(&["Freehold", "Leasehold"]),
|
||
description: "Whether the property is leasehold or freehold",
|
||
detail: "From HM Land Registry Price Paid data. Freehold means you own the building and the land it stands on. Leasehold means you own the building but not the land — you have a lease from the freeholder for a set number of years.",
|
||
source: "price-paid",
|
||
},
|
||
EnumFeatureConfig {
|
||
name: "Property type",
|
||
order: Some(&["Detached", "Semi-Detached", "Terraced", "Flats/Maisonettes"]),
|
||
description: "Type of property: detached, semi-detached, terraced, or flat/maisonette",
|
||
detail: "From HM Land Registry Price Paid data. The broad property type classification: Detached, Semi-Detached, Terraced, or Flats/Maisonettes.",
|
||
source: "price-paid",
|
||
},
|
||
EnumFeatureConfig {
|
||
name: "Current energy rating",
|
||
order: Some(&["A", "B", "C", "D", "E", "F", "G"]),
|
||
description: "Current EPC energy efficiency rating (A = best, G = worst)",
|
||
detail: "The current energy efficiency rating from the Energy Performance Certificate. Ranges from A (most efficient) to G (least efficient). Based on the property's energy use per square metre of floor area.",
|
||
source: "epc",
|
||
},
|
||
EnumFeatureConfig {
|
||
name: "Potential energy rating",
|
||
order: Some(&["A", "B", "C", "D", "E", "F", "G"]),
|
||
description: "Potential EPC rating if all recommended improvements were made",
|
||
detail: "The potential energy efficiency rating from the Energy Performance Certificate if all cost-effective improvements recommended in the EPC report were carried out. Ranges from A (most efficient) to G (least efficient).",
|
||
source: "epc",
|
||
},
|
||
],
|
||
},
|
||
EnumFeatureGroup {
|
||
name: "Environment",
|
||
features: &[
|
||
EnumFeatureConfig {
|
||
name: "Environmental risk",
|
||
order: Some(&["Low", "Moderate", "Significant"]),
|
||
description: "Highest ground stability risk across all six hazard types",
|
||
detail: "Overall ground stability risk for the area, taken as the maximum across all six GeoSure hazard categories (collapsible deposits, compressible ground, landslides, running sand, shrink-swell, and soluble rocks). From Ordnance Survey GeoSure data on a 5km hex grid.",
|
||
source: "geosure",
|
||
},
|
||
EnumFeatureConfig {
|
||
name: "Collapsible deposits risk",
|
||
order: Some(&["Low", "Moderate", "Significant"]),
|
||
description: "Risk of ground collapse from natural underground cavities",
|
||
detail: "From OS GeoSure. Indicates the likelihood of ground collapse due to natural cavities formed by dissolution of soluble rocks or the collapse of old mines and natural pipes. Rated on a 5km hex grid across Great Britain.",
|
||
source: "geosure",
|
||
},
|
||
EnumFeatureConfig {
|
||
name: "Compressible ground risk",
|
||
order: Some(&["Low", "Moderate", "Significant"]),
|
||
description: "Risk of ground compression causing subsidence",
|
||
detail: "From OS GeoSure. Indicates the potential for ground to compress under loading, which can cause gradual settlement or subsidence of buildings and infrastructure. Typically associated with soft clay, silt, or peat deposits.",
|
||
source: "geosure",
|
||
},
|
||
EnumFeatureConfig {
|
||
name: "Landslide risk",
|
||
order: Some(&["Low", "Moderate", "Significant"]),
|
||
description: "Risk of landslide or slope instability",
|
||
detail: "From OS GeoSure. Indicates the susceptibility of the ground to landslides and slope instability. Based on slope angle, geology, and historical landslide records.",
|
||
source: "geosure",
|
||
},
|
||
EnumFeatureConfig {
|
||
name: "Running sand risk",
|
||
order: Some(&["Low", "Moderate", "Significant"]),
|
||
description: "Risk of sand becoming fluid when saturated",
|
||
detail: "From OS GeoSure. Indicates the potential for fine-grained sand to behave like a fluid when saturated with water, which can affect excavations and foundations.",
|
||
source: "geosure",
|
||
},
|
||
EnumFeatureConfig {
|
||
name: "Shrink-swell risk",
|
||
order: Some(&["Low", "Moderate", "Significant"]),
|
||
description: "Risk of clay shrinking and swelling with moisture changes",
|
||
detail: "From OS GeoSure. Indicates the potential for clay-rich soils to shrink when dry and swell when wet, causing ground movement that can damage buildings and infrastructure. One of the most common causes of subsidence in the UK.",
|
||
source: "geosure",
|
||
},
|
||
EnumFeatureConfig {
|
||
name: "Soluble rocks risk",
|
||
order: Some(&["Low", "Moderate", "Significant"]),
|
||
description: "Risk of sinkholes from dissolution of soluble rocks",
|
||
detail: "From OS GeoSure. Indicates the potential for soluble rocks (limestone, chalk, gypsum) to dissolve, creating underground voids that can lead to sinkholes and ground subsidence.",
|
||
source: "geosure",
|
||
},
|
||
],
|
||
},
|
||
];
|
||
|
||
/// Flat ordered list of all numeric feature names (follows group order).
|
||
pub fn all_numeric_feature_names() -> Vec<&'static str> {
|
||
FEATURE_GROUPS
|
||
.iter()
|
||
.flat_map(|group| group.features.iter().map(|feature| feature.name))
|
||
.collect()
|
||
}
|
||
|
||
/// Flat ordered list of all enum feature names (follows group order).
|
||
pub fn all_enum_feature_names() -> Vec<&'static str> {
|
||
ENUM_FEATURE_GROUPS
|
||
.iter()
|
||
.flat_map(|group| group.features.iter().map(|feature| feature.name))
|
||
.collect()
|
||
}
|
||
|
||
/// Look up the configured value order for an enum feature by name.
|
||
pub fn order_for(name: &str) -> Option<&'static [&'static str]> {
|
||
ENUM_FEATURE_GROUPS
|
||
.iter()
|
||
.flat_map(|group| group.features.iter())
|
||
.find(|feature| feature.name == name)
|
||
.and_then(|feature| feature.order)
|
||
}
|
||
|
||
/// Whether this feature should use integer-width histogram bins.
|
||
pub fn has_integer_bins(name: &str) -> bool {
|
||
INTEGER_BIN_FEATURES.contains(&name)
|
||
}
|
||
|
||
/// Look up the Bounds config for a numeric feature by name.
|
||
pub fn bounds_for(name: &str) -> Option<&'static Bounds> {
|
||
FEATURE_GROUPS
|
||
.iter()
|
||
.flat_map(|group| group.features.iter())
|
||
.find(|feature| feature.name == name)
|
||
.map(|feature| &feature.bounds)
|
||
}
|
||
|
||
/// Canonical display order for POI category groups.
|
||
/// The server will panic at startup if the data contains groups not in this list or vice versa.
|
||
pub const POI_GROUP_ORDER: &[&str] = &[
|
||
"Public Transport",
|
||
"Leisure",
|
||
"Education",
|
||
"Health",
|
||
"Emergency Services",
|
||
"Other",
|
||
"Groceries",
|
||
"Local Businesses",
|
||
"Culture",
|
||
"Services",
|
||
"Shops",
|
||
];
|