server
This commit is contained in:
parent
8dc939d761
commit
d98819b569
12 changed files with 157 additions and 161 deletions
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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}, \
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue