1319 lines
58 KiB
Rust
1319 lines
58 KiB
Rust
//! Static feature configuration. Every numeric and enum column in wide.parquet
|
|
//! must be declared here. Unknown columns cause a startup panic.
|
|
|
|
#[derive(Clone, Copy)]
|
|
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"];
|
|
|
|
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,
|
|
}
|
|
|
|
/// Wrapper enum allowing numeric and enum features to be interleaved
|
|
/// in a single ordered array. The position in the array determines
|
|
/// the display order on the frontend.
|
|
pub enum Feature {
|
|
Numeric(FeatureConfig),
|
|
Enum(EnumFeatureConfig),
|
|
}
|
|
|
|
pub struct FeatureGroup {
|
|
pub name: &'static str,
|
|
pub features: &'static [Feature],
|
|
}
|
|
|
|
pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|
FeatureGroup {
|
|
name: "Transport",
|
|
features: &[
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Distance to nearest train or tube station (km)",
|
|
bounds: Bounds::Percentile {
|
|
low: 2.0,
|
|
high: 98.0,
|
|
},
|
|
step: 0.1,
|
|
description: "Distance to the closest train or tube station",
|
|
detail: "Straight-line distance in kilometres from the postcode to the nearest rail station or Tube/metro/tram stop.",
|
|
source: "naptan",
|
|
prefix: "",
|
|
suffix: " km",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
],
|
|
},
|
|
FeatureGroup {
|
|
name: "Properties",
|
|
features: &[
|
|
Feature::Enum(EnumFeatureConfig {
|
|
name: "Property type",
|
|
order: Some(&["Detached", "Semi-Detached", "Terraced", "Flats/Maisonettes", "Other"]),
|
|
description: "Type of property: detached, semi-detached, terraced, flat/maisonette, or other",
|
|
detail: "From HM Land Registry Price Paid data and EPC certificates. Detached, Semi-Detached, Terraced (includes all terrace sub-types), Flats/Maisonettes, or Other (bungalows, park homes, etc.).",
|
|
source: "price-paid",
|
|
}),
|
|
Feature::Enum(EnumFeatureConfig {
|
|
name: "Leasehold/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",
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Estimated current price",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 2_500_000.0,
|
|
},
|
|
step: 10000.0,
|
|
description: "Modelled estimate of the current property value",
|
|
detail: "Based on the last sale price, local repeat-sales price movement, and nearby recently sold properties. The repeat-sales index is tracked by postcode sector and property type, with smoothing and neighbour blending where data is sparse. Recent sales stay close to the recorded price; older sales depend more on the model.",
|
|
source: "price-paid",
|
|
prefix: "£",
|
|
suffix: "",
|
|
raw: false,
|
|
absolute: true,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Last known price",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 2_500_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. May be years old if the property hasn't sold recently.",
|
|
source: "price-paid",
|
|
prefix: "£",
|
|
suffix: "",
|
|
raw: false,
|
|
absolute: true,
|
|
}),
|
|
Feature::Numeric(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 modelled estimated current price 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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Estimated monthly rent",
|
|
bounds: Bounds::Percentile { low: 2.0, high: 98.0 },
|
|
step: 25.0,
|
|
description: "Mean monthly private rent for the local area",
|
|
detail: "Mean monthly rental price from ONS Price Index of Private Rents (PIPR), matched by local authority and bedroom count.",
|
|
source: "ons-rental",
|
|
prefix: "£",
|
|
suffix: "/mo",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Construction year",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 2026.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Estimated year of construction from the EPC",
|
|
detail: "Derived from the construction age band in the EPC (e.g. '1930-1949') by taking the midpoint. Less precise for older buildings where the age band spans several decades.",
|
|
source: "epc",
|
|
prefix: "",
|
|
suffix: "",
|
|
raw: true,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Enum(EnumFeatureConfig {
|
|
name: "Former council house",
|
|
order: Some(&["Yes", "No"]),
|
|
description: "Whether the property was ever recorded as social housing",
|
|
detail: "Derived from the TENURE field in Energy Performance Certificate data. If any EPC certificate for this property recorded the tenure as social rental, it indicates the property was council or housing-association stock at the time of that inspection. Properties that were later sold (e.g. via Right to Buy) retain this flag.",
|
|
source: "epc",
|
|
}),
|
|
Feature::Enum(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",
|
|
}),
|
|
Feature::Enum(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",
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
],
|
|
},
|
|
FeatureGroup {
|
|
name: "Education",
|
|
features: &[
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Good+ primary schools within 2km",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 10.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Primary schools rated Good or Outstanding by Ofsted within 2km",
|
|
detail: "State-funded primary schools within 2km with a current Ofsted rating of Good or Outstanding. Schools not yet inspected are excluded.",
|
|
source: "ofsted",
|
|
prefix: "",
|
|
suffix: "",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Good+ secondary schools within 2km",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 5.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Secondary schools rated Good or Outstanding by Ofsted within 2km",
|
|
detail: "State-funded secondary schools within 2km with a current Ofsted rating of Good or Outstanding. Schools not yet inspected are excluded.",
|
|
source: "ofsted",
|
|
prefix: "",
|
|
suffix: "",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Outstanding primary schools within 2km",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 10.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Primary schools rated Outstanding by Ofsted within 2km",
|
|
detail: "State-funded primary schools within 2km with a current Ofsted rating of Outstanding. Schools not yet inspected are excluded.",
|
|
source: "ofsted",
|
|
prefix: "",
|
|
suffix: "",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Outstanding secondary schools within 2km",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 5.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Secondary schools rated Outstanding by Ofsted within 2km",
|
|
detail: "State-funded secondary schools within 2km with a current Ofsted rating of Outstanding. Schools not yet inspected are excluded.",
|
|
source: "ofsted",
|
|
prefix: "",
|
|
suffix: "",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(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 within 5km",
|
|
detail: "State-funded primary schools within 5km with a current Ofsted rating of Good or Outstanding. Schools not yet inspected are excluded.",
|
|
source: "ofsted",
|
|
prefix: "",
|
|
suffix: "",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(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 within 5km",
|
|
detail: "State-funded secondary schools within 5km with a current Ofsted rating of Good or Outstanding. Schools not yet inspected are excluded.",
|
|
source: "ofsted",
|
|
prefix: "",
|
|
suffix: "",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Outstanding primary schools within 5km",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 30.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Primary schools rated Outstanding by Ofsted within 5km",
|
|
detail: "State-funded primary schools within 5km with a current Ofsted rating of Outstanding. Schools not yet inspected are excluded.",
|
|
source: "ofsted",
|
|
prefix: "",
|
|
suffix: "",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Outstanding secondary schools within 5km",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 15.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Secondary schools rated Outstanding by Ofsted within 5km",
|
|
detail: "State-funded secondary schools within 5km with a current Ofsted rating of Outstanding. Schools not yet inspected are excluded.",
|
|
source: "ofsted",
|
|
prefix: "",
|
|
suffix: "",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Education, Skills and Training Score",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Education and skills deprivation percentile (higher = less deprived)",
|
|
detail: "From the English Indices of Deprivation, converted to a national percentile where 0% is most deprived and 100% is least deprived. Covers school attainment, entry to higher education, adult qualifications, and English language proficiency.",
|
|
source: "iod",
|
|
prefix: "",
|
|
suffix: "%",
|
|
raw: true,
|
|
absolute: true,
|
|
}),
|
|
],
|
|
},
|
|
FeatureGroup {
|
|
name: "Deprivation",
|
|
features: &[
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Income Score",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Income deprivation percentile (higher = less deprived)",
|
|
detail: "From the English Indices of Deprivation, converted to a national percentile where 0% is most income deprived and 100% is least income deprived. 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: true,
|
|
absolute: true,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Employment Score",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Employment deprivation percentile (higher = less deprived)",
|
|
detail: "From the English Indices of Deprivation, converted to a national percentile where 0% is most employment deprived and 100% is least employment deprived. 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: true,
|
|
absolute: true,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Health Deprivation and Disability Score",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Health and disability deprivation percentile (higher = better outcomes)",
|
|
detail: "From the English Indices of Deprivation, converted to a national percentile where 0% is most health deprived and 100% is least health deprived. Derived from years of potential life lost, comparative illness and disability ratio, acute morbidity, and mood and anxiety disorders.",
|
|
source: "iod",
|
|
prefix: "",
|
|
suffix: "%",
|
|
raw: true,
|
|
absolute: true,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Housing Conditions Score",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Housing conditions percentile (higher = better conditions)",
|
|
detail: "From the English Indices of Deprivation, Living Environment domain, converted to a national percentile where 0% is most deprived and 100% is least deprived. Measures the quality of housing stock: central heating availability, housing condition, and Decent Homes standards.",
|
|
source: "iod",
|
|
prefix: "",
|
|
suffix: "%",
|
|
raw: true,
|
|
absolute: true,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Air Quality and Road Safety Score",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Air quality and road safety percentile (higher = better conditions)",
|
|
detail: "From the English Indices of Deprivation, Living Environment domain, converted to a national percentile where 0% is most deprived and 100% is least deprived. Measures the outdoor living environment through air quality indicators and road traffic accident casualties involving pedestrians and cyclists.",
|
|
source: "iod",
|
|
prefix: "",
|
|
suffix: "%",
|
|
raw: true,
|
|
absolute: true,
|
|
}),
|
|
],
|
|
},
|
|
FeatureGroup {
|
|
name: "Crime",
|
|
features: &[
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Serious crime per 1k residents (avg/yr)",
|
|
bounds: Bounds::Percentile {
|
|
low: 2.0,
|
|
high: 98.0,
|
|
},
|
|
step: 0.1,
|
|
description: "Serious crime rate per 1,000 residents per year",
|
|
detail: "Violence, robbery, burglary, and weapons possession per 1,000 usual residents per year in the LSOA. Uses police.uk street-level crime data (2023-2025) and Census 2021 population counts. Normalises for population density so areas are comparable regardless of size.",
|
|
source: "crime",
|
|
prefix: "",
|
|
suffix: "/yr",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Minor crime per 1k residents (avg/yr)",
|
|
bounds: Bounds::Percentile {
|
|
low: 2.0,
|
|
high: 98.0,
|
|
},
|
|
step: 0.1,
|
|
description: "Minor crime rate per 1,000 residents per year",
|
|
detail: "Anti-social behaviour, shoplifting, bicycle theft, and other lower-severity crime per 1,000 usual residents per year in the LSOA. Uses police.uk street-level crime data (2023-2025) and Census 2021 population counts. Normalises for population density so areas are comparable regardless of size.",
|
|
source: "crime",
|
|
prefix: "",
|
|
suffix: "/yr",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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: &[
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Median age",
|
|
bounds: Bounds::Percentile {
|
|
low: 2.0,
|
|
high: 98.0,
|
|
},
|
|
step: 0.5,
|
|
description: "Median age of the local population",
|
|
detail: "From the 2021 Census (TS007A). Median age of usual residents in the LSOA, computed by linear interpolation from five-year age band counts. Areas with younger populations tend to be urban, university towns, or have more families; older medians are typical in rural and coastal areas.",
|
|
source: "census-2021",
|
|
prefix: "",
|
|
suffix: " years",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "% South Asian",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 0.1,
|
|
description: "Percentage of population identifying as South Asian",
|
|
detail: "From the 2021 Census. Percentage of the local authority population identifying as Indian, Pakistani, Bangladeshi, or any other Asian background.",
|
|
source: "ethnicity",
|
|
prefix: "",
|
|
suffix: "%",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "% Black",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 0.1,
|
|
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,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "% East Asian",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 0.1,
|
|
description: "Percentage of population identifying as East Asian",
|
|
detail: "From the 2021 Census. Percentage of the local authority population identifying as Chinese.",
|
|
source: "ethnicity",
|
|
prefix: "",
|
|
suffix: "%",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "% Mixed",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 0.1,
|
|
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,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "% Other",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 0.1,
|
|
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: "Politics",
|
|
features: &[
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "% Labour",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Labour vote share in the 2024 General Election",
|
|
detail: "Percentage of valid votes cast for the Labour Party in the constituency covering this postcode, from the July 2024 UK General Election. Includes votes for all Labour candidates where multiple stood.",
|
|
source: "election-results",
|
|
prefix: "",
|
|
suffix: "%",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "% Conservative",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Conservative vote share in the 2024 General Election",
|
|
detail: "Percentage of valid votes cast for the Conservative Party in the constituency covering this postcode, from the July 2024 UK General Election.",
|
|
source: "election-results",
|
|
prefix: "",
|
|
suffix: "%",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "% Liberal Democrat",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Liberal Democrat vote share in the 2024 General Election",
|
|
detail: "Percentage of valid votes cast for the Liberal Democrats in the constituency covering this postcode, from the July 2024 UK General Election.",
|
|
source: "election-results",
|
|
prefix: "",
|
|
suffix: "%",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "% Reform UK",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Reform UK vote share in the 2024 General Election",
|
|
detail: "Percentage of valid votes cast for Reform UK in the constituency covering this postcode, from the July 2024 UK General Election.",
|
|
source: "election-results",
|
|
prefix: "",
|
|
suffix: "%",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "% Green",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Green Party vote share in the 2024 General Election",
|
|
detail: "Percentage of valid votes cast for the Green Party in the constituency covering this postcode, from the July 2024 UK General Election.",
|
|
source: "election-results",
|
|
prefix: "",
|
|
suffix: "%",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "% Other parties",
|
|
bounds: Bounds::Fixed {
|
|
min: 0.0,
|
|
max: 100.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Combined vote share of all other parties and independents",
|
|
detail: "Percentage of valid votes cast for parties other than Labour, Conservative, Liberal Democrat, Reform UK, and Green in the constituency covering this postcode. Includes independents, the Speaker, and smaller parties.",
|
|
source: "election-results",
|
|
prefix: "",
|
|
suffix: "%",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Voter turnout (%)",
|
|
bounds: Bounds::Percentile {
|
|
low: 2.0,
|
|
high: 98.0,
|
|
},
|
|
step: 0.5,
|
|
description:
|
|
"Percentage of registered voters who voted in the 2024 General Election",
|
|
detail: "The proportion of the registered electorate who cast a valid vote in the July 2024 UK General Election. Calculated as valid votes divided by electorate size. Higher turnout generally correlates with more affluent areas and closer contests.",
|
|
source: "election-results",
|
|
prefix: "",
|
|
suffix: "%",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
],
|
|
},
|
|
FeatureGroup {
|
|
name: "Amenities",
|
|
features: &[
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Distance to nearest park (km)",
|
|
bounds: Bounds::Percentile {
|
|
low: 2.0,
|
|
high: 98.0,
|
|
},
|
|
step: 0.1,
|
|
description: "Distance to the closest park or green space",
|
|
detail: "Straight-line distance in kilometres from the postcode to the nearest park entrance. Covers public parks, gardens, playing fields, and play spaces. Uses access point locations from the OS Open Greenspace dataset, so properties bordering a large park correctly show a short distance.",
|
|
source: "os-open-greenspace",
|
|
prefix: "",
|
|
suffix: " km",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Distance to nearest grocery store (km)",
|
|
bounds: Bounds::Percentile {
|
|
low: 2.0,
|
|
high: 98.0,
|
|
},
|
|
step: 0.1,
|
|
description: "Distance to the closest grocery shop or supermarket",
|
|
detail: "Straight-line distance in kilometres from the postcode to the nearest grocery shop, supermarket, or convenience store. Uses OpenStreetMap POIs, with Waitrose and Tesco coverage from GEOLYTIX retail points.",
|
|
source: "osm-pois",
|
|
prefix: "",
|
|
suffix: " km",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Distance to nearest tube station (km)",
|
|
bounds: Bounds::Percentile {
|
|
low: 2.0,
|
|
high: 98.0,
|
|
},
|
|
step: 0.1,
|
|
description: "Distance to the closest Tube, metro, tram, or DLR stop",
|
|
detail: "Straight-line distance in kilometres from the postcode to the nearest NaPTAN station classified as Tube, metro, tram, or DLR.",
|
|
source: "naptan",
|
|
prefix: "",
|
|
suffix: " km",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Distance to nearest rail station (km)",
|
|
bounds: Bounds::Percentile {
|
|
low: 2.0,
|
|
high: 98.0,
|
|
},
|
|
step: 0.1,
|
|
description: "Distance to the closest National Rail station",
|
|
detail: "Straight-line distance in kilometres from the postcode to the nearest NaPTAN railway station.",
|
|
source: "naptan",
|
|
prefix: "",
|
|
suffix: " km",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Distance to nearest Waitrose (km)",
|
|
bounds: Bounds::Percentile {
|
|
low: 2.0,
|
|
high: 98.0,
|
|
},
|
|
step: 0.1,
|
|
description: "Distance to the closest Waitrose store",
|
|
detail: "Straight-line distance in kilometres from the postcode to the nearest Waitrose or Little Waitrose store in the GEOLYTIX Grocery Retail Points dataset.",
|
|
source: "geolytix-retail-points",
|
|
prefix: "",
|
|
suffix: " km",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Distance to nearest Tesco (km)",
|
|
bounds: Bounds::Percentile {
|
|
low: 2.0,
|
|
high: 98.0,
|
|
},
|
|
step: 0.1,
|
|
description: "Distance to the closest Tesco store",
|
|
detail: "Straight-line distance in kilometres from the postcode to the nearest Tesco store in the GEOLYTIX Grocery Retail Points dataset.",
|
|
source: "geolytix-retail-points",
|
|
prefix: "",
|
|
suffix: " km",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Distance to nearest cafe (km)",
|
|
bounds: Bounds::Percentile {
|
|
low: 2.0,
|
|
high: 98.0,
|
|
},
|
|
step: 0.1,
|
|
description: "Distance to the closest cafe",
|
|
detail: "Straight-line distance in kilometres from the postcode to the nearest cafe, ice-cream shop, or internet cafe mapped in OpenStreetMap.",
|
|
source: "osm-pois",
|
|
prefix: "",
|
|
suffix: " km",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Distance to nearest pub (km)",
|
|
bounds: Bounds::Percentile {
|
|
low: 2.0,
|
|
high: 98.0,
|
|
},
|
|
step: 0.1,
|
|
description: "Distance to the closest pub",
|
|
detail: "Straight-line distance in kilometres from the postcode to the nearest pub, social club, brewery, distillery, or winery mapped in OpenStreetMap.",
|
|
source: "osm-pois",
|
|
prefix: "",
|
|
suffix: " km",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Distance to nearest restaurant (km)",
|
|
bounds: Bounds::Percentile {
|
|
low: 2.0,
|
|
high: 98.0,
|
|
},
|
|
step: 0.1,
|
|
description: "Distance to the closest restaurant",
|
|
detail: "Straight-line distance in kilometres from the postcode to the nearest restaurant or food court mapped in OpenStreetMap.",
|
|
source: "osm-pois",
|
|
prefix: "",
|
|
suffix: " km",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(FeatureConfig {
|
|
name: "Number of parks within 1km",
|
|
bounds: Bounds::Percentile {
|
|
low: 5.0,
|
|
high: 95.0,
|
|
},
|
|
step: 1.0,
|
|
description: "Number of parks and green spaces within 1km",
|
|
detail: "Count of public parks, gardens, playing fields, and play spaces with at least one entrance within a 1km radius of the property's postcode centroid. Derived from the OS Open Greenspace dataset (Ordnance Survey), using park entrance locations for accurate proximity matching.",
|
|
source: "os-open-greenspace",
|
|
prefix: "",
|
|
suffix: "",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(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: "Restaurants, cafes, and food establishments within 2km of the postcode. Sourced from OpenStreetMap.",
|
|
source: "osm-pois",
|
|
prefix: "",
|
|
suffix: "",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Numeric(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,
|
|
}),
|
|
Feature::Numeric(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, a 24-hour weighted average) from Defra's Strategic Noise Mapping Round 4 (2022). Modelled at 4m above ground on a 10m grid. Above ~55 dB is typically noticeable; above ~70 dB is considered harmful by the WHO.",
|
|
source: "noise",
|
|
prefix: "",
|
|
suffix: " dB",
|
|
raw: false,
|
|
absolute: false,
|
|
}),
|
|
Feature::Enum(EnumFeatureConfig {
|
|
name: "Max available download speed (Mbps)",
|
|
order: Some(&["10", "30", "100", "300", "1000"]),
|
|
description: "Maximum broadband download speed available at the postcode",
|
|
detail: "Maximum fixed broadband download speed available from any provider, from Ofcom Connected Nations 2025. Represents theoretical maximum, not achieved speeds. 10 Mbps = basic, 30 = superfast, 100+ = ultrafast, 1000 = gigabit.",
|
|
source: "broadband",
|
|
}),
|
|
],
|
|
},
|
|
];
|
|
|
|
/// 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())
|
|
.filter_map(|feature| match feature {
|
|
Feature::Numeric(c) => Some(c.name),
|
|
Feature::Enum(_) => None,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Flat ordered list of all enum feature names (follows group order).
|
|
pub fn all_enum_feature_names() -> Vec<&'static str> {
|
|
FEATURE_GROUPS
|
|
.iter()
|
|
.flat_map(|group| group.features.iter())
|
|
.filter_map(|feature| match feature {
|
|
Feature::Enum(c) => Some(c.name),
|
|
Feature::Numeric(_) => None,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Look up the configured value order for an enum feature by name.
|
|
pub fn order_for(name: &str) -> Option<&'static [&'static str]> {
|
|
FEATURE_GROUPS
|
|
.iter()
|
|
.flat_map(|group| group.features.iter())
|
|
.find_map(|feature| match feature {
|
|
Feature::Enum(c) if c.name == name => Some(c.order),
|
|
_ => None,
|
|
})
|
|
.flatten()
|
|
}
|
|
|
|
/// Whether this feature should use integer-width histogram bins.
|
|
pub fn has_integer_bins(name: &str) -> bool {
|
|
INTEGER_BIN_FEATURES.contains(&name) || dynamic_poi_count_radius(name).is_some()
|
|
}
|
|
|
|
/// Look up the Bounds config for a numeric feature by name.
|
|
pub fn bounds_for(name: &str) -> Option<Bounds> {
|
|
if dynamic_poi_distance_category(name).is_some() {
|
|
return Some(Bounds::Percentile {
|
|
low: 2.0,
|
|
high: 98.0,
|
|
});
|
|
}
|
|
if dynamic_poi_count_radius(name).is_some() {
|
|
return Some(Bounds::Percentile {
|
|
low: 5.0,
|
|
high: 95.0,
|
|
});
|
|
}
|
|
|
|
FEATURE_GROUPS
|
|
.iter()
|
|
.flat_map(|group| group.features.iter())
|
|
.find_map(|feature| match feature {
|
|
Feature::Numeric(c) if c.name == name => Some(c.bounds),
|
|
_ => None,
|
|
})
|
|
}
|
|
|
|
pub fn dynamic_poi_distance_category(name: &str) -> Option<&str> {
|
|
name.strip_prefix("Distance to nearest ")
|
|
.and_then(|rest| rest.strip_suffix(" POI (km)"))
|
|
.filter(|category| !category.is_empty())
|
|
}
|
|
|
|
pub fn dynamic_poi_count_radius(name: &str) -> Option<u8> {
|
|
let rest = name.strip_prefix("Number of ")?;
|
|
let (_category, suffix) = rest.rsplit_once(" POIs within ")?;
|
|
match suffix {
|
|
"2km" => Some(2),
|
|
"5km" => Some(5),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn dynamic_poi_count_category(name: &str) -> Option<&str> {
|
|
let rest = name.strip_prefix("Number of ")?;
|
|
let (category, suffix) = rest.rsplit_once(" POIs within ")?;
|
|
matches!(suffix, "2km" | "5km")
|
|
.then_some(category)
|
|
.filter(|category| !category.is_empty())
|
|
}
|
|
|
|
pub fn is_dynamic_poi_feature(name: &str) -> bool {
|
|
dynamic_poi_distance_category(name).is_some() || dynamic_poi_count_category(name).is_some()
|
|
}
|
|
|
|
pub fn dynamic_poi_feature_sort_key(name: &str) -> (u8, String) {
|
|
if let Some(category) = dynamic_poi_distance_category(name) {
|
|
return (0, category.to_ascii_lowercase());
|
|
}
|
|
if let Some(category) = dynamic_poi_count_category(name) {
|
|
let metric_order = match dynamic_poi_count_radius(name) {
|
|
Some(2) => 1,
|
|
Some(5) => 2,
|
|
_ => 3,
|
|
};
|
|
return (metric_order, category.to_ascii_lowercase());
|
|
}
|
|
(9, name.to_ascii_lowercase())
|
|
}
|
|
|
|
/// 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",
|
|
"Groceries",
|
|
"Leisure",
|
|
"Education",
|
|
"Health",
|
|
"Emergency Services",
|
|
"Other",
|
|
"Local Businesses",
|
|
"Culture",
|
|
"Services",
|
|
"Shops",
|
|
];
|