server
Some checks failed
Build and publish Docker image / build-and-push (push) Failing after 6m7s
CI / Check (push) Failing after 7m21s

This commit is contained in:
Andras Schmelczer 2026-05-31 13:19:26 +01:00
parent 8dc939d761
commit d98819b569
12 changed files with 157 additions and 161 deletions

View file

@ -55,6 +55,16 @@ pub async fn get_actual_listings(
) -> Result<Json<ActualListingsResponse>, Response> {
let state = shared.load_state();
let offset = params.offset.unwrap_or(0);
// Gate the entire feature behind the per-user `can_see_listings` flag. The
// flag is off by default for everyone, so listings are invisible unless a
// superuser has explicitly granted access to this account.
if !user.0.as_ref().is_some_and(|u| u.can_see_listings) {
return Err(
ApiError::Forbidden("You do not have access to listings".to_string()).into_response(),
);
}
let Some(actual_listings) = state.actual_listings.clone() else {
return Ok(Json(ActualListingsResponse {
listings: Vec::new(),

View file

@ -359,9 +359,9 @@ pub fn build_system_prompt(
or \"max\" (at most this value). Never set two filters on the same feature.\n\
- Use EXACT feature names from the list spelling, capitalisation, and punctuation must match.\n\
- \"cheap\" / \"affordable\" = lower price range. \"expensive\" = higher price range.\n\
- \"low crime\" / \"safe\" = low values on the Serious crime and Minor crime features. \
Prefer the per-1k resident crime features for broad area safety; use specific crime \
features only when the user names a crime type.\n\
- \"low crime\" / \"safe\" = low values on the Serious crime (avg/yr) and Minor crime (avg/yr) \
features (incidents counted within 50m of the postcode). Prefer these aggregates for broad \
area safety; use specific crime features only when the user names a crime type.\n\
- \"quiet\" = low Noise (dB). \"green\" / \"near parks\" = high Number of amenities (Park) within 2km \
or low Distance to nearest park (km), depending on wording.\n\
- \"good schools\" = Good+ school features. \"outstanding schools\" = Outstanding school features.\n\
@ -505,8 +505,8 @@ pub fn build_system_prompt(
parts.push(
"\nUser: \"safe quiet area with good schools and parks\"\n\
Output: {\"numeric_filters\": [\
{\"name\": \"Serious crime per 1k residents (avg/yr)\", \"bound\": \"max\", \"value\": 20}, \
{\"name\": \"Minor crime per 1k residents (avg/yr)\", \"bound\": \"max\", \"value\": 50}, \
{\"name\": \"Serious crime (avg/yr)\", \"bound\": \"max\", \"value\": 5}, \
{\"name\": \"Minor crime (avg/yr)\", \"bound\": \"max\", \"value\": 20}, \
{\"name\": \"Noise (dB)\", \"bound\": \"max\", \"value\": 55}, \
{\"name\": \"Good+ primary schools within 2km\", \"bound\": \"min\", \"value\": 2}, \
{\"name\": \"Good+ secondary schools within 2km\", \"bound\": \"min\", \"value\": 1}, \

View file

@ -13,6 +13,7 @@ pub enum OverlayTileFormat {
VectorMvtGzip,
RasterPng,
RasterJpeg,
RasterWebp,
}
impl OverlayTileFormat {
@ -21,6 +22,7 @@ impl OverlayTileFormat {
Self::VectorMvtGzip => "application/x-protobuf",
Self::RasterPng => "image/png",
Self::RasterJpeg => "image/jpeg",
Self::RasterWebp => "image/webp",
}
}
@ -30,15 +32,11 @@ impl OverlayTileFormat {
}
pub async fn get_overlay_tile(
reader: Option<Arc<TileReader>>,
reader: Arc<TileReader>,
format: OverlayTileFormat,
overlay_name: &'static str,
Path((zoom, col, row)): Path<(u8, u32, u32)>,
) -> Response {
let Some(reader) = reader else {
return StatusCode::NOT_FOUND.into_response();
};
let tile_coord = match TileCoord::new(zoom, col, row) {
Ok(tile_coord) => tile_coord,
Err(err) => {

View file

@ -227,6 +227,7 @@ pub async fn get_places(
.postcodes
.iter()
.filter(|postcode| postcode_starts_with_compact(postcode, &compact_query))
.filter(|postcode| !property_data.rows_for_postcode(postcode).is_empty())
.take(limit)
.cloned()
.collect()

View file

@ -123,7 +123,15 @@ fn insert_feature_value(
return;
}
let value = state.data.get_feature(row, feat_idx);
// `get_feature` decodes the lossy u16-quantized value, which turns round
// sale prices into noise (e.g. £428,000 → £427,984). For the last sale we
// keep an unquantized copy, so serve that instead to match the exact
// `historical_prices` entries and the price-history chart.
let value = if feature_names[feat_idx] == "Last known price" {
state.data.last_known_price_raw(row)
} else {
state.data.get_feature(row, feat_idx)
};
if value.is_finite() {
features.insert(feature_names[feat_idx].clone(), value);
}

View file

@ -257,10 +257,11 @@ pub fn compute_feature_stats(
/// Compute property-weighted per-year crime means across the selection.
///
/// Each matching property contributes its LSOA's per-year counts; this is the
/// same property-weighted-LSOA-average shape used elsewhere in the right pane.
/// LSOAs with no series for a given crime type contribute 0 for that type
/// (matching how the existing `(avg/yr)` columns treat missing crime types).
/// Each matching property contributes its postcode's per-year counts (incidents
/// within 50m of that postcode); this is the same property-weighted-average
/// shape used elsewhere in the right pane. Postcodes with no series for a given
/// crime type contribute 0 for that type (matching how the `(avg/yr)` columns
/// treat missing crime types).
pub fn compute_crime_by_year(
matching_rows: &[usize],
data: &PropertyData,
@ -273,19 +274,19 @@ pub fn compute_crime_by_year(
}
// For each crime type, accumulate per-year sums and the count of rows whose
// LSOA exists in the crime side table.
// postcode exists in the crime side table.
let num_types = crime_by_year.crime_types.len();
let mut per_type_year_sums: Vec<FxHashMap<i32, f64>> =
(0..num_types).map(|_| FxHashMap::default()).collect();
let mut per_type_row_counts: Vec<u32> = vec![0; num_types];
for &row in matching_rows {
let lsoa = data.lsoa(row);
let Some(series_list) = crime_by_year.series_by_lsoa.get(lsoa) else {
let postcode = data.postcode(row);
let Some(series_list) = crime_by_year.series_by_postcode.get(postcode) else {
continue;
};
// For every type the LSOA reports, add its per-year counts.
// For every type the postcode reports, add its per-year counts.
// For types it doesn't report, treat the row as contributing 0 — so we
// bump the row count for *every* known type below.
for series in series_list {