Deploy again

This commit is contained in:
Andras Schmelczer 2026-02-19 22:24:06 +00:00
parent ffe080adef
commit 787428f1a5
18 changed files with 717 additions and 223 deletions

1
server-rs/Cargo.lock generated
View file

@ -2450,6 +2450,7 @@ dependencies = [
"hex",
"hmac",
"lasso",
"libc",
"metrics",
"metrics-exporter-prometheus",
"parking_lot",

View file

@ -31,6 +31,7 @@ hmac = "0.12"
sha2 = "0.10"
hex = "0.4"
tower = { version = "0.5", features = ["limit"] }
libc = "0.2"
[lints.clippy]
min_ident_chars = "warn"

View file

@ -0,0 +1,522 @@
2026-02-19T21:26:54.458535Z INFO property_map_server: Prometheus metrics initialized
2026-02-19T21:26:54.458730Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
2026-02-19T21:26:54.458738Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-02-19T21:26:54.560667Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
2026-02-19T21:26:54.560677Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-02-19T21:27:01.536771Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
2026-02-19T21:27:01.536788Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
2026-02-19T21:27:01.858493Z INFO property_map_server::data::property: buy listings joined rows=444605
2026-02-19T21:27:01.858503Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
2026-02-19T21:27:01.970421Z INFO property_map_server::data::property: rent listings joined rows=125656
2026-02-19T21:27:01.970430Z INFO property_map_server::data::property: Concatenating all data sources
2026-02-19T21:27:52.277322Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
2026-02-19T21:27:52.277425Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
2026-02-19T21:27:53.590317Z INFO property_map_server::data::property: Combined data selected rows=15773642
2026-02-19T21:27:53.731832Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-02-19T21:27:58.340459Z INFO property_map_server::data::property: Computing histograms for numeric features
2026-02-19T21:27:59.454079Z INFO property_map_server::data::property: Extracting string columns
2026-02-19T21:28:01.589530Z INFO property_map_server::data::property: Building enum features
2026-02-19T21:29:17.412343Z INFO property_map_server::data::property: Extracting renovation history
2026-02-19T21:29:19.625773Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829807
2026-02-19T21:29:19.625780Z INFO property_map_server::data::property: Extracting listing features
2026-02-19T21:29:21.764449Z INFO property_map_server::data::property: Listing features extracted properties_with_features=508871
2026-02-19T21:29:21.764457Z INFO property_map_server::data::property: Sorting rows by spatial locality
2026-02-19T21:29:28.223647Z INFO property_map_server::data::property: Building interned strings
2026-02-19T21:30:05.730665Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted)
2026-02-19T21:32:02.361349Z INFO property_map_server::data::property: Data loading complete
2026-02-19T21:32:03.976002Z INFO property_map_server: Property data loaded rows=15773642 features=67 enums=12
2026-02-19T21:32:03.976011Z INFO property_map_server: Building spatial grid index (0.01° cells)
2026-02-19T21:32:04.076953Z INFO property_map_server: Precomputing H3 cells at resolution 12
2026-02-19T21:32:04.076963Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
2026-02-19T21:32:04.487571Z INFO property_map_server::data::property: H3 precomputation complete (15773642 cells)
2026-02-19T21:32:04.490452Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
2026-02-19T21:32:04.490466Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
2026-02-19T21:32:04.740771Z INFO property_map_server::data::poi: Loaded 811937 POIs
2026-02-19T21:32:04.865428Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71
2026-02-19T21:32:04.866050Z INFO property_map_server::data::poi: POI data loading complete.
2026-02-19T21:32:04.896188Z INFO property_map_server: POI data loaded pois=811937
2026-02-19T21:32:04.896196Z INFO property_map_server: Building POI spatial grid index
2026-02-19T21:32:04.903631Z INFO property_map_server: Loading place data from /app/data/places.parquet
2026-02-19T21:32:04.908488Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
2026-02-19T21:32:04.916283Z INFO property_map_server::data::places: Loaded 90807 places
2026-02-19T21:32:04.951763Z INFO property_map_server::data::places: Place data loaded places=90807 types=11 with_population=2112 with_city=87577
2026-02-19T21:32:04.952866Z INFO property_map_server: Place data loaded places=90807
2026-02-19T21:32:04.952882Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
2026-02-19T21:32:04.952983Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
2026-02-19T21:32:04.956401Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
2026-02-19T21:32:07.669253Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
2026-02-19T21:32:07.669264Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
2026-02-19T21:32:07.669278Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
2026-02-19T21:32:07.677413Z INFO property_map_server: PMTiles loaded successfully
2026-02-19T21:32:07.729382Z INFO property_map_server: No --dist provided; static serving and OG injection disabled
2026-02-19T21:32:07.792213Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
2026-02-19T21:32:07.792458Z INFO property_map_server: Precomputed features response groups=9
2026-02-19T21:32:07.792535Z INFO property_map_server: Precomputed AI filters schema and system prompt
2026-02-19T21:32:07.796454Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
2026-02-19T21:32:13.118788Z INFO property_map_server::pocketbase: PocketBase users collection already has is_admin, subscription, and newsletter fields
2026-02-19T21:32:13.169893Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
2026-02-19T21:32:13.169910Z INFO property_map_server::pocketbase: PocketBase collection 'invites' already exists
2026-02-19T21:32:13.169913Z INFO property_map_server::pocketbase: PocketBase collection 'short_urls' already exists
2026-02-19T21:32:13.230971Z WARN property_map_server::pocketbase: PocketBase settings missing oauth2.providers array — cannot configure OAuth
2026-02-19T21:32:13.230981Z INFO property_map_server: Ollama configured: http://host.docker.internal:11434 (model: gpt-oss:20b)
2026-02-19T21:32:13.230992Z INFO property_map_server: Loading travel time data from /app/data/travel-times
2026-02-19T21:32:13.251928Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=76039
2026-02-19T21:32:13.272182Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=76039
2026-02-19T21:32:13.291900Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=69290
2026-02-19T21:32:13.291945Z INFO property_map_server: Travel time store loaded modes=3
2026-02-19T21:32:13.296486Z INFO property_map_server: Server listening on 0.0.0.0:8001
2026-02-19T21:32:14.023023Z INFO property_map_server::routes::features: GET /api/features
2026-02-19T21:32:16.790595Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=5 cells_before_filter=687 cells_after_filter=687 truncated=false bounds=47.0000,-14.0000,57.0000,10.0000 filters=0 filters_raw="-" travel_entries=0 agg_ms=1729.5 total_ms=1748.8
2026-02-19T21:32:23.004683Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11
2026-02-19T21:32:24.013755Z INFO property_map_server::routes::features: GET /api/features
2026-02-19T21:32:24.013792Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11
2026-02-19T21:32:25.049834Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=10 cells_before_filter=1419 cells_after_filter=1259 truncated=false bounds=51.4908,-0.1363,51.5292,-0.0637 filters=0 filters_raw="-" travel_entries=0 agg_ms=11.2 total_ms=38.8
2026-02-19T21:36:25.033898Z INFO property_map_server: Prometheus metrics initialized
2026-02-19T21:36:25.034069Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
2026-02-19T21:36:25.034079Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-02-19T21:36:25.129912Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
2026-02-19T21:36:25.129921Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-02-19T21:36:32.499133Z INFO property_map_server: Prometheus metrics initialized
2026-02-19T21:36:32.499319Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
2026-02-19T21:36:32.499330Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-02-19T21:36:32.558294Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
2026-02-19T21:36:32.558304Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-02-19T21:36:34.752324Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
2026-02-19T21:36:34.752339Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
2026-02-19T21:36:35.020717Z INFO property_map_server::data::property: buy listings joined rows=444605
2026-02-19T21:36:35.020727Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
2026-02-19T21:36:35.117921Z INFO property_map_server::data::property: rent listings joined rows=125656
2026-02-19T21:36:35.117931Z INFO property_map_server::data::property: Concatenating all data sources
2026-02-19T21:38:50.238928Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
2026-02-19T21:38:50.239009Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
2026-02-19T21:38:51.489522Z INFO property_map_server::data::property: Combined data selected rows=15773642
2026-02-19T21:38:51.661991Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-02-19T21:39:38.714760Z INFO property_map_server: Prometheus metrics initialized
2026-02-19T21:39:38.714936Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
2026-02-19T21:39:38.714944Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-02-19T21:39:38.790493Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
2026-02-19T21:39:38.790504Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-02-19T21:39:41.014520Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
2026-02-19T21:39:41.014538Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
2026-02-19T21:39:41.299979Z INFO property_map_server::data::property: buy listings joined rows=444605
2026-02-19T21:39:41.299990Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
2026-02-19T21:39:41.400048Z INFO property_map_server::data::property: rent listings joined rows=125656
2026-02-19T21:39:41.400059Z INFO property_map_server::data::property: Concatenating all data sources
2026-02-19T21:39:47.989385Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
2026-02-19T21:39:47.989481Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
2026-02-19T21:39:49.243773Z INFO property_map_server::data::property: Combined data selected rows=15773642
2026-02-19T21:39:49.422488Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-02-19T21:39:54.408464Z INFO property_map_server::data::property: Computing histograms for numeric features
2026-02-19T21:39:55.491023Z INFO property_map_server::data::property: Extracting string columns
2026-02-19T21:39:57.641424Z INFO property_map_server::data::property: Building enum features
2026-02-19T21:40:09.935990Z INFO property_map_server::data::property: Extracting renovation history
2026-02-19T21:40:12.074163Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829807
2026-02-19T21:40:12.074172Z INFO property_map_server::data::property: Extracting listing features
2026-02-19T21:40:12.681574Z INFO property_map_server::data::property: Listing features extracted properties_with_features=508871
2026-02-19T21:40:12.681588Z INFO property_map_server::data::property: Sorting rows by spatial locality
2026-02-19T21:40:18.238722Z INFO property_map_server::data::property: Building interned strings
2026-02-19T21:40:24.556588Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted)
2026-02-19T21:40:52.861550Z INFO property_map_server::data::property: Data loading complete
2026-02-19T21:40:54.156096Z INFO property_map_server: Property data loaded rows=15773642 features=67 enums=12
2026-02-19T21:40:54.156105Z INFO property_map_server: Building spatial grid index (0.01° cells)
2026-02-19T21:40:54.550391Z INFO property_map_server: Precomputing H3 cells at resolution 12
2026-02-19T21:40:54.550401Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
2026-02-19T21:40:54.950194Z INFO property_map_server::data::property: H3 precomputation complete (15773642 cells)
2026-02-19T21:40:54.950226Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
2026-02-19T21:40:54.950233Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
2026-02-19T21:40:54.970688Z INFO property_map_server::data::poi: Loaded 811937 POIs
2026-02-19T21:40:55.091891Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71
2026-02-19T21:40:55.092505Z INFO property_map_server::data::poi: POI data loading complete.
2026-02-19T21:40:55.122637Z INFO property_map_server: POI data loaded pois=811937
2026-02-19T21:40:55.122650Z INFO property_map_server: Building POI spatial grid index
2026-02-19T21:40:55.132909Z INFO property_map_server: Loading place data from /app/data/places.parquet
2026-02-19T21:40:55.132919Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
2026-02-19T21:40:55.135615Z INFO property_map_server::data::places: Loaded 90807 places
2026-02-19T21:40:55.155573Z INFO property_map_server::data::places: Place data loaded places=90807 types=11 with_population=2112 with_city=87577
2026-02-19T21:40:55.157182Z INFO property_map_server: Place data loaded places=90807
2026-02-19T21:40:55.157198Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
2026-02-19T21:40:55.157202Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
2026-02-19T21:40:55.169027Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
2026-02-19T21:40:56.700286Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
2026-02-19T21:40:56.700297Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
2026-02-19T21:40:56.700310Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
2026-02-19T21:40:56.711874Z INFO property_map_server: PMTiles loaded successfully
2026-02-19T21:40:56.767004Z INFO property_map_server: No --dist provided; static serving and OG injection disabled
2026-02-19T21:40:56.793907Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
2026-02-19T21:40:56.794046Z INFO property_map_server: Precomputed features response groups=9
2026-02-19T21:40:56.794089Z INFO property_map_server: Precomputed AI filters schema and system prompt
2026-02-19T21:40:56.794100Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
2026-02-19T21:40:56.883854Z INFO property_map_server::pocketbase: PocketBase users collection already has is_admin, subscription, and newsletter fields
2026-02-19T21:40:56.887425Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
2026-02-19T21:40:56.887435Z INFO property_map_server::pocketbase: PocketBase collection 'invites' already exists
2026-02-19T21:40:56.887438Z INFO property_map_server::pocketbase: PocketBase collection 'short_urls' already exists
2026-02-19T21:40:56.936330Z WARN property_map_server::pocketbase: PocketBase settings missing oauth2.providers array — cannot configure OAuth
2026-02-19T21:40:56.936343Z INFO property_map_server: Ollama configured: http://host.docker.internal:11434 (model: gpt-oss:20b)
2026-02-19T21:40:56.936362Z INFO property_map_server: Loading travel time data from /app/data/travel-times
2026-02-19T21:40:57.078090Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=76039
2026-02-19T21:40:57.241363Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=76039
2026-02-19T21:40:57.424132Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=69290
2026-02-19T21:40:57.424164Z INFO property_map_server: Travel time store loaded modes=3
2026-02-19T21:40:57.424333Z INFO property_map_server: Server listening on 0.0.0.0:8001
2026-02-19T21:45:48.088981Z INFO property_map_server: Prometheus metrics initialized
2026-02-19T21:45:48.089157Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
2026-02-19T21:45:48.089163Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-02-19T21:45:48.151222Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
2026-02-19T21:45:48.151231Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-02-19T21:45:50.419725Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
2026-02-19T21:45:50.419740Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
2026-02-19T21:45:50.680792Z INFO property_map_server::data::property: buy listings joined rows=444605
2026-02-19T21:45:50.680801Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
2026-02-19T21:45:50.790235Z INFO property_map_server::data::property: rent listings joined rows=125656
2026-02-19T21:45:50.790245Z INFO property_map_server::data::property: Concatenating all data sources
2026-02-19T21:45:59.531271Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
2026-02-19T21:45:59.531351Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
2026-02-19T21:46:00.677779Z INFO property_map_server::data::property: Combined data selected rows=15773642
2026-02-19T21:46:00.823682Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-02-19T21:46:11.566611Z INFO property_map_server: Prometheus metrics initialized
2026-02-19T21:46:11.566786Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
2026-02-19T21:46:11.566792Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-02-19T21:46:11.644730Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
2026-02-19T21:46:11.644739Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-02-19T21:46:17.296113Z INFO property_map_server: Prometheus metrics initialized
2026-02-19T21:46:17.296298Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
2026-02-19T21:46:17.296309Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-02-19T21:46:17.355178Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
2026-02-19T21:46:17.355187Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-02-19T21:46:19.508288Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
2026-02-19T21:46:19.508307Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
2026-02-19T21:46:19.775415Z INFO property_map_server::data::property: buy listings joined rows=444605
2026-02-19T21:46:19.775424Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
2026-02-19T21:46:19.877913Z INFO property_map_server::data::property: rent listings joined rows=125656
2026-02-19T21:46:19.877923Z INFO property_map_server::data::property: Concatenating all data sources
2026-02-19T21:46:22.229279Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
2026-02-19T21:46:22.229352Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
2026-02-19T21:46:23.385234Z INFO property_map_server::data::property: Combined data selected rows=15773642
2026-02-19T21:46:23.566673Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-02-19T21:46:28.957436Z INFO property_map_server::data::property: Computing histograms for numeric features
2026-02-19T21:46:34.625853Z INFO property_map_server: Prometheus metrics initialized
2026-02-19T21:46:34.626033Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
2026-02-19T21:46:34.626039Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-02-19T21:46:34.683165Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
2026-02-19T21:46:34.683174Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-02-19T21:46:39.619046Z INFO property_map_server: Prometheus metrics initialized
2026-02-19T21:46:39.619206Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
2026-02-19T21:46:39.619211Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-02-19T21:46:39.682402Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
2026-02-19T21:46:39.682412Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-02-19T21:46:41.896969Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
2026-02-19T21:46:41.896985Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
2026-02-19T21:46:42.158027Z INFO property_map_server::data::property: buy listings joined rows=444605
2026-02-19T21:46:42.158037Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
2026-02-19T21:46:42.256671Z INFO property_map_server::data::property: rent listings joined rows=125656
2026-02-19T21:46:42.256682Z INFO property_map_server::data::property: Concatenating all data sources
2026-02-19T21:46:44.596786Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
2026-02-19T21:46:44.596858Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
2026-02-19T21:46:45.729422Z INFO property_map_server::data::property: Combined data selected rows=15773642
2026-02-19T21:46:45.884768Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-02-19T21:46:51.133252Z INFO property_map_server::data::property: Computing histograms for numeric features
2026-02-19T21:46:52.129246Z INFO property_map_server::data::property: Extracting string columns
2026-02-19T21:46:54.247724Z INFO property_map_server::data::property: Building enum features
2026-02-19T21:47:06.556048Z INFO property_map_server::data::property: Extracting renovation history
2026-02-19T21:47:08.635978Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829807
2026-02-19T21:47:08.635986Z INFO property_map_server::data::property: Extracting listing features
2026-02-19T21:47:09.231804Z INFO property_map_server::data::property: Listing features extracted properties_with_features=508871
2026-02-19T21:47:09.231812Z INFO property_map_server::data::property: Sorting rows by spatial locality
2026-02-19T21:47:14.793765Z INFO property_map_server::data::property: Building interned strings
2026-02-19T21:47:21.074369Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted)
2026-02-19T21:47:49.305297Z INFO property_map_server::data::property: Data loading complete
2026-02-19T21:47:50.726389Z INFO property_map_server: Property data loaded rows=15773642 features=67 enums=12
2026-02-19T21:47:50.726398Z INFO property_map_server: Building spatial grid index (0.01° cells)
2026-02-19T21:47:50.825433Z INFO property_map_server: Precomputing H3 cells at resolution 12
2026-02-19T21:47:50.825442Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
2026-02-19T21:47:51.183337Z INFO property_map_server::data::property: H3 precomputation complete (15773642 cells)
2026-02-19T21:47:51.183366Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
2026-02-19T21:47:51.183377Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
2026-02-19T21:47:51.239629Z INFO property_map_server::data::poi: Loaded 811937 POIs
2026-02-19T21:47:51.358287Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71
2026-02-19T21:47:51.358921Z INFO property_map_server::data::poi: POI data loading complete.
2026-02-19T21:47:51.389530Z INFO property_map_server: POI data loaded pois=811937
2026-02-19T21:47:51.389537Z INFO property_map_server: Building POI spatial grid index
2026-02-19T21:47:51.397611Z INFO property_map_server: Loading place data from /app/data/places.parquet
2026-02-19T21:47:51.397621Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
2026-02-19T21:47:51.404147Z INFO property_map_server::data::places: Loaded 90807 places
2026-02-19T21:47:51.422097Z INFO property_map_server::data::places: Place data loaded places=90807 types=11 with_population=2112 with_city=87577
2026-02-19T21:47:51.423272Z INFO property_map_server: Place data loaded places=90807
2026-02-19T21:47:51.423286Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
2026-02-19T21:47:51.423293Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
2026-02-19T21:47:51.427524Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
2026-02-19T21:47:52.459962Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
2026-02-19T21:47:52.459974Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
2026-02-19T21:47:52.459991Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
2026-02-19T21:47:52.460299Z INFO property_map_server: PMTiles loaded successfully
2026-02-19T21:47:52.509697Z INFO property_map_server: No --dist provided; static serving and OG injection disabled
2026-02-19T21:47:52.548802Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
2026-02-19T21:47:52.548955Z INFO property_map_server: Precomputed features response groups=9
2026-02-19T21:47:52.548998Z INFO property_map_server: Precomputed AI filters schema and system prompt
2026-02-19T21:47:52.549010Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
2026-02-19T21:47:52.651249Z INFO property_map_server::pocketbase: PocketBase users collection already has is_admin, subscription, and newsletter fields
2026-02-19T21:47:52.657303Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
2026-02-19T21:47:52.657312Z INFO property_map_server::pocketbase: PocketBase collection 'invites' already exists
2026-02-19T21:47:52.657314Z INFO property_map_server::pocketbase: PocketBase collection 'short_urls' already exists
2026-02-19T21:47:52.699918Z WARN property_map_server::pocketbase: PocketBase settings missing oauth2.providers array — cannot configure OAuth
2026-02-19T21:47:52.699928Z INFO property_map_server: Ollama configured: http://host.docker.internal:11434 (model: gpt-oss:20b)
2026-02-19T21:47:52.699941Z INFO property_map_server: Loading travel time data from /app/data/travel-times
2026-02-19T21:47:52.776587Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=76039
2026-02-19T21:47:52.892688Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=76039
2026-02-19T21:47:52.988301Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=69290
2026-02-19T21:47:52.988332Z INFO property_map_server: Travel time store loaded modes=3
2026-02-19T21:47:52.988500Z INFO property_map_server: Server listening on 0.0.0.0:8001
2026-02-19T21:51:21.780285Z INFO property_map_server: Prometheus metrics initialized
2026-02-19T21:51:21.780462Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
2026-02-19T21:51:21.780471Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-02-19T21:51:21.851737Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
2026-02-19T21:51:21.851746Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-02-19T21:51:24.076503Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
2026-02-19T21:51:24.076522Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
2026-02-19T21:51:24.334934Z INFO property_map_server::data::property: buy listings joined rows=444605
2026-02-19T21:51:24.334944Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
2026-02-19T21:51:24.438503Z INFO property_map_server::data::property: rent listings joined rows=125656
2026-02-19T21:51:24.438513Z INFO property_map_server::data::property: Concatenating all data sources
2026-02-19T21:51:35.892299Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
2026-02-19T21:51:35.892405Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
2026-02-19T21:51:36.985627Z INFO property_map_server::data::property: Combined data selected rows=15773642
2026-02-19T21:51:37.144498Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-02-19T21:51:42.441195Z INFO property_map_server::data::property: Computing histograms for numeric features
2026-02-19T21:51:43.421336Z INFO property_map_server::data::property: Extracting string columns
2026-02-19T21:51:45.512575Z INFO property_map_server::data::property: Building enum features
2026-02-19T21:51:57.729162Z INFO property_map_server::data::property: Extracting renovation history
2026-02-19T21:51:59.870295Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829807
2026-02-19T21:51:59.870303Z INFO property_map_server::data::property: Extracting listing features
2026-02-19T21:52:00.496544Z INFO property_map_server::data::property: Listing features extracted properties_with_features=508871
2026-02-19T21:52:00.496553Z INFO property_map_server::data::property: Sorting rows by spatial locality
2026-02-19T21:52:05.810063Z INFO property_map_server::data::property: Building interned strings
2026-02-19T21:52:12.478733Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted)
2026-02-19T21:52:57.041288Z INFO property_map_server::data::property: Data loading complete
2026-02-19T21:52:58.190166Z INFO property_map_server: Property data loaded rows=15773642 features=67 enums=12
2026-02-19T21:52:58.190227Z INFO property_map_server: Building spatial grid index (0.01° cells)
2026-02-19T21:52:58.612625Z INFO property_map_server: Precomputing H3 cells at resolution 12
2026-02-19T21:52:58.612634Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
2026-02-19T21:52:59.010323Z INFO property_map_server::data::property: H3 precomputation complete (15773642 cells)
2026-02-19T21:52:59.010352Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
2026-02-19T21:52:59.010357Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
2026-02-19T21:52:59.033154Z INFO property_map_server::data::poi: Loaded 811937 POIs
2026-02-19T21:52:59.168962Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71
2026-02-19T21:52:59.169620Z INFO property_map_server::data::poi: POI data loading complete.
2026-02-19T21:52:59.200062Z INFO property_map_server: POI data loaded pois=811937
2026-02-19T21:52:59.200069Z INFO property_map_server: Building POI spatial grid index
2026-02-19T21:52:59.209004Z INFO property_map_server: Loading place data from /app/data/places.parquet
2026-02-19T21:52:59.209026Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
2026-02-19T21:52:59.217593Z INFO property_map_server::data::places: Loaded 90807 places
2026-02-19T21:52:59.237507Z INFO property_map_server::data::places: Place data loaded places=90807 types=11 with_population=2112 with_city=87577
2026-02-19T21:52:59.238659Z INFO property_map_server: Place data loaded places=90807
2026-02-19T21:52:59.238673Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
2026-02-19T21:52:59.238677Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
2026-02-19T21:52:59.239461Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
2026-02-19T21:53:00.577770Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
2026-02-19T21:53:00.577784Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
2026-02-19T21:53:00.577800Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
2026-02-19T21:53:00.578044Z INFO property_map_server: PMTiles loaded successfully
2026-02-19T21:53:00.635812Z INFO property_map_server: No --dist provided; static serving and OG injection disabled
2026-02-19T21:53:00.661112Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
2026-02-19T21:53:00.661276Z INFO property_map_server: Precomputed features response groups=9
2026-02-19T21:53:00.661327Z INFO property_map_server: Precomputed AI filters schema and system prompt
2026-02-19T21:53:00.661341Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
2026-02-19T21:53:00.777123Z INFO property_map_server::pocketbase: PocketBase users collection already has is_admin, subscription, and newsletter fields
2026-02-19T21:53:00.780442Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
2026-02-19T21:53:00.780453Z INFO property_map_server::pocketbase: PocketBase collection 'invites' already exists
2026-02-19T21:53:00.780455Z INFO property_map_server::pocketbase: PocketBase collection 'short_urls' already exists
2026-02-19T21:53:00.827713Z WARN property_map_server::pocketbase: PocketBase settings missing oauth2.providers array — cannot configure OAuth
2026-02-19T21:53:00.827728Z INFO property_map_server: Ollama configured: http://host.docker.internal:11434 (model: gpt-oss:20b)
2026-02-19T21:53:00.827756Z INFO property_map_server: Loading travel time data from /app/data/travel-times
2026-02-19T21:53:00.853494Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=76039
2026-02-19T21:53:00.875117Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=76039
2026-02-19T21:53:00.897287Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=69290
2026-02-19T21:53:00.897342Z INFO property_map_server: Travel time store loaded modes=3
2026-02-19T21:53:00.897521Z WARN property_map_server: mlockall failed (need CAP_IPC_LOCK or sufficient RLIMIT_MEMLOCK): Cannot allocate memory (os error 12)
2026-02-19T21:53:00.897564Z INFO property_map_server: Server listening on 0.0.0.0:8001
2026-02-19T21:53:43.607361Z INFO property_map_server: Prometheus metrics initialized
2026-02-19T21:53:43.607524Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
2026-02-19T21:53:43.607533Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-02-19T21:53:43.703745Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
2026-02-19T21:53:43.703756Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-02-19T21:53:46.126315Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
2026-02-19T21:53:46.126336Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
2026-02-19T21:53:46.404697Z INFO property_map_server::data::property: buy listings joined rows=444605
2026-02-19T21:53:46.404708Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
2026-02-19T21:53:46.508191Z INFO property_map_server::data::property: rent listings joined rows=125656
2026-02-19T21:53:46.508203Z INFO property_map_server::data::property: Concatenating all data sources
2026-02-19T21:54:04.815379Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
2026-02-19T21:54:04.815453Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
2026-02-19T21:54:05.957615Z INFO property_map_server::data::property: Combined data selected rows=15773642
2026-02-19T21:54:06.114182Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-02-19T21:54:11.113430Z INFO property_map_server::data::property: Computing histograms for numeric features
2026-02-19T21:54:12.014906Z INFO property_map_server::data::property: Extracting string columns
2026-02-19T21:54:14.114892Z INFO property_map_server::data::property: Building enum features
2026-02-19T21:54:26.216628Z INFO property_map_server::data::property: Extracting renovation history
2026-02-19T21:54:28.295021Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829807
2026-02-19T21:54:28.295030Z INFO property_map_server::data::property: Extracting listing features
2026-02-19T21:54:28.894967Z INFO property_map_server::data::property: Listing features extracted properties_with_features=508871
2026-02-19T21:54:28.894975Z INFO property_map_server::data::property: Sorting rows by spatial locality
2026-02-19T21:54:34.492548Z INFO property_map_server::data::property: Building interned strings
2026-02-19T21:54:40.775643Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted)
2026-02-19T21:55:09.150090Z INFO property_map_server::data::property: Data loading complete
2026-02-19T21:55:10.482353Z INFO property_map_server: Property data loaded rows=15773642 features=67 enums=12
2026-02-19T21:55:10.482362Z INFO property_map_server: Building spatial grid index (0.01° cells)
2026-02-19T21:55:10.579983Z INFO property_map_server: Precomputing H3 cells at resolution 12
2026-02-19T21:55:10.579992Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
2026-02-19T21:55:10.931346Z INFO property_map_server::data::property: H3 precomputation complete (15773642 cells)
2026-02-19T21:55:10.931371Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
2026-02-19T21:55:10.931376Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
2026-02-19T21:55:10.956383Z INFO property_map_server::data::poi: Loaded 811937 POIs
2026-02-19T21:55:11.076557Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71
2026-02-19T21:55:11.077176Z INFO property_map_server::data::poi: POI data loading complete.
2026-02-19T21:55:11.107885Z INFO property_map_server: POI data loaded pois=811937
2026-02-19T21:55:11.107892Z INFO property_map_server: Building POI spatial grid index
2026-02-19T21:55:11.115230Z INFO property_map_server: Loading place data from /app/data/places.parquet
2026-02-19T21:55:11.115239Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
2026-02-19T21:55:11.117955Z INFO property_map_server::data::places: Loaded 90807 places
2026-02-19T21:55:11.135514Z INFO property_map_server::data::places: Place data loaded places=90807 types=11 with_population=2112 with_city=87577
2026-02-19T21:55:11.136623Z INFO property_map_server: Place data loaded places=90807
2026-02-19T21:55:11.136637Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
2026-02-19T21:55:11.136641Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
2026-02-19T21:55:11.138240Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
2026-02-19T21:55:12.410370Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
2026-02-19T21:55:12.410381Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
2026-02-19T21:55:12.410398Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
2026-02-19T21:55:12.410625Z INFO property_map_server: PMTiles loaded successfully
2026-02-19T21:55:12.462925Z INFO property_map_server: No --dist provided; static serving and OG injection disabled
2026-02-19T21:55:12.494298Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
2026-02-19T21:55:12.494439Z INFO property_map_server: Precomputed features response groups=9
2026-02-19T21:55:12.494482Z INFO property_map_server: Precomputed AI filters schema and system prompt
2026-02-19T21:55:12.494496Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
2026-02-19T21:55:12.541473Z INFO property_map_server::pocketbase: PocketBase users collection already has is_admin, subscription, and newsletter fields
2026-02-19T21:55:12.543245Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
2026-02-19T21:55:12.543253Z INFO property_map_server::pocketbase: PocketBase collection 'invites' already exists
2026-02-19T21:55:12.543255Z INFO property_map_server::pocketbase: PocketBase collection 'short_urls' already exists
2026-02-19T21:55:12.586588Z WARN property_map_server::pocketbase: PocketBase settings missing oauth2.providers array — cannot configure OAuth
2026-02-19T21:55:12.586599Z INFO property_map_server: Ollama configured: http://host.docker.internal:11434 (model: gpt-oss:20b)
2026-02-19T21:55:12.586612Z INFO property_map_server: Loading travel time data from /app/data/travel-times
2026-02-19T21:55:12.608664Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=76039
2026-02-19T21:55:12.629502Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=76039
2026-02-19T21:55:12.648844Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=69290
2026-02-19T21:55:12.648881Z INFO property_map_server: Travel time store loaded modes=3
2026-02-19T21:55:12.649064Z WARN property_map_server: mlockall failed (need CAP_IPC_LOCK or sufficient RLIMIT_MEMLOCK): Cannot allocate memory (os error 12)
2026-02-19T21:55:12.649102Z INFO property_map_server: Server listening on 0.0.0.0:8001
2026-02-19T21:56:27.019313Z INFO property_map_server: Prometheus metrics initialized
2026-02-19T21:56:27.019464Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
2026-02-19T21:56:27.019473Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-02-19T21:56:27.077642Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
2026-02-19T21:56:27.077651Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-02-19T21:56:29.376382Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
2026-02-19T21:56:29.376400Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
2026-02-19T21:56:29.643314Z INFO property_map_server::data::property: buy listings joined rows=444605
2026-02-19T21:56:29.643325Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
2026-02-19T21:56:29.750729Z INFO property_map_server::data::property: rent listings joined rows=125656
2026-02-19T21:56:29.750740Z INFO property_map_server::data::property: Concatenating all data sources
2026-02-19T21:56:36.764563Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
2026-02-19T21:56:36.764643Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
2026-02-19T21:56:37.882852Z INFO property_map_server::data::property: Combined data selected rows=15773642
2026-02-19T21:56:38.041464Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-02-19T21:56:43.567623Z INFO property_map_server::data::property: Computing histograms for numeric features
2026-02-19T21:56:44.547629Z INFO property_map_server::data::property: Extracting string columns
2026-02-19T21:56:46.638073Z INFO property_map_server::data::property: Building enum features
2026-02-19T21:56:58.743576Z INFO property_map_server::data::property: Extracting renovation history
2026-02-19T21:57:00.886254Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829807
2026-02-19T21:57:00.886263Z INFO property_map_server::data::property: Extracting listing features
2026-02-19T21:57:01.500069Z INFO property_map_server::data::property: Listing features extracted properties_with_features=508871
2026-02-19T21:57:01.500077Z INFO property_map_server::data::property: Sorting rows by spatial locality
2026-02-19T21:57:07.085398Z INFO property_map_server::data::property: Building interned strings
2026-02-19T21:57:13.333355Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted)
2026-02-19T21:57:42.416818Z INFO property_map_server::data::property: Data loading complete
2026-02-19T21:57:43.429909Z INFO property_map_server: Property data loaded rows=15773642 features=67 enums=12
2026-02-19T21:57:43.429917Z INFO property_map_server: Building spatial grid index (0.01° cells)
2026-02-19T21:57:43.809875Z INFO property_map_server: Precomputing H3 cells at resolution 12
2026-02-19T21:57:43.809884Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
2026-02-19T21:57:44.161951Z INFO property_map_server::data::property: H3 precomputation complete (15773642 cells)
2026-02-19T21:57:44.161990Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
2026-02-19T21:57:44.161997Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
2026-02-19T21:57:44.184832Z INFO property_map_server::data::poi: Loaded 811937 POIs
2026-02-19T21:57:44.303890Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71
2026-02-19T21:57:44.304500Z INFO property_map_server::data::poi: POI data loading complete.
2026-02-19T21:57:44.335104Z INFO property_map_server: POI data loaded pois=811937
2026-02-19T21:57:44.335113Z INFO property_map_server: Building POI spatial grid index
2026-02-19T21:57:44.342807Z INFO property_map_server: Loading place data from /app/data/places.parquet
2026-02-19T21:57:44.342817Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
2026-02-19T21:57:44.345902Z INFO property_map_server::data::places: Loaded 90807 places
2026-02-19T21:57:44.365611Z INFO property_map_server::data::places: Place data loaded places=90807 types=11 with_population=2112 with_city=87577
2026-02-19T21:57:44.368113Z INFO property_map_server: Place data loaded places=90807
2026-02-19T21:57:44.368128Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
2026-02-19T21:57:44.368133Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
2026-02-19T21:57:44.368931Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
2026-02-19T21:57:45.407981Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
2026-02-19T21:57:45.407992Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
2026-02-19T21:57:45.408009Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
2026-02-19T21:57:45.408231Z INFO property_map_server: PMTiles loaded successfully
2026-02-19T21:57:45.458216Z INFO property_map_server: No --dist provided; static serving and OG injection disabled
2026-02-19T21:57:45.483167Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
2026-02-19T21:57:45.483325Z INFO property_map_server: Precomputed features response groups=9
2026-02-19T21:57:45.483362Z INFO property_map_server: Precomputed AI filters schema and system prompt
2026-02-19T21:57:45.483372Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
2026-02-19T21:57:45.527603Z INFO property_map_server::pocketbase: PocketBase users collection already has is_admin, subscription, and newsletter fields
2026-02-19T21:57:45.529274Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
2026-02-19T21:57:45.529282Z INFO property_map_server::pocketbase: PocketBase collection 'invites' already exists
2026-02-19T21:57:45.529284Z INFO property_map_server::pocketbase: PocketBase collection 'short_urls' already exists
2026-02-19T22:00:02.316188Z INFO property_map_server: Prometheus metrics initialized
2026-02-19T22:00:02.316363Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
2026-02-19T22:00:02.316376Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-02-19T22:00:02.374613Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
2026-02-19T22:00:02.374622Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-02-19T22:00:04.644372Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
2026-02-19T22:00:04.644386Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
2026-02-19T22:00:04.911541Z INFO property_map_server::data::property: buy listings joined rows=444605
2026-02-19T22:00:04.911554Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
2026-02-19T22:00:05.012469Z INFO property_map_server::data::property: rent listings joined rows=125656
2026-02-19T22:00:05.012480Z INFO property_map_server::data::property: Concatenating all data sources
2026-02-19T22:00:22.135033Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
2026-02-19T22:00:22.135120Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
2026-02-19T22:00:23.338993Z INFO property_map_server::data::property: Combined data selected rows=15773642
2026-02-19T22:00:23.510508Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-02-19T22:00:28.484323Z INFO property_map_server::data::property: Computing histograms for numeric features
2026-02-19T22:00:29.357751Z INFO property_map_server::data::property: Extracting string columns
2026-02-19T22:00:31.494852Z INFO property_map_server::data::property: Building enum features
2026-02-19T22:00:43.668748Z INFO property_map_server::data::property: Extracting renovation history
2026-02-19T22:00:45.723371Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829807
2026-02-19T22:00:45.723379Z INFO property_map_server::data::property: Extracting listing features
2026-02-19T22:00:46.332508Z INFO property_map_server::data::property: Listing features extracted properties_with_features=508871
2026-02-19T22:00:46.332517Z INFO property_map_server::data::property: Sorting rows by spatial locality
2026-02-19T22:00:51.842163Z INFO property_map_server::data::property: Building interned strings
2026-02-19T22:00:57.978323Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted)
2026-02-19T22:01:26.614095Z INFO property_map_server::data::property: Data loading complete
2026-02-19T22:01:27.675542Z INFO property_map_server: Property data loaded rows=15773642 features=67 enums=12
2026-02-19T22:01:27.675552Z INFO property_map_server: Building spatial grid index (0.01° cells)
2026-02-19T22:01:28.060580Z INFO property_map_server: Precomputing H3 cells at resolution 12
2026-02-19T22:01:28.060590Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
2026-02-19T22:01:28.424039Z INFO property_map_server::data::property: H3 precomputation complete (15773642 cells)
2026-02-19T22:01:28.424101Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
2026-02-19T22:01:28.424183Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
2026-02-19T22:01:28.448672Z INFO property_map_server::data::poi: Loaded 811937 POIs
2026-02-19T22:01:28.566180Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71
2026-02-19T22:01:28.566791Z INFO property_map_server::data::poi: POI data loading complete.
2026-02-19T22:01:28.596675Z INFO property_map_server: POI data loaded pois=811937
2026-02-19T22:01:28.596685Z INFO property_map_server: Building POI spatial grid index
2026-02-19T22:01:28.603824Z INFO property_map_server: Loading place data from /app/data/places.parquet
2026-02-19T22:01:28.603830Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
2026-02-19T22:01:28.606465Z INFO property_map_server::data::places: Loaded 90807 places
2026-02-19T22:01:28.623823Z INFO property_map_server::data::places: Place data loaded places=90807 types=11 with_population=2112 with_city=87577
2026-02-19T22:01:28.624990Z INFO property_map_server: Place data loaded places=90807
2026-02-19T22:01:28.625002Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
2026-02-19T22:01:28.625006Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
2026-02-19T22:01:28.637363Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
2026-02-19T22:01:29.656030Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
2026-02-19T22:01:29.656042Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
2026-02-19T22:01:29.656058Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
2026-02-19T22:01:29.656288Z INFO property_map_server: PMTiles loaded successfully
2026-02-19T22:01:29.705260Z INFO property_map_server: No --dist provided; static serving and OG injection disabled
2026-02-19T22:01:29.738938Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
2026-02-19T22:01:29.739087Z INFO property_map_server: Precomputed features response groups=9
2026-02-19T22:01:29.739137Z INFO property_map_server: Precomputed AI filters schema and system prompt
2026-02-19T22:01:29.739149Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
2026-02-19T22:01:29.786529Z INFO property_map_server::pocketbase: PocketBase users collection already has is_admin, subscription, and newsletter fields
2026-02-19T22:01:29.788719Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
2026-02-19T22:01:29.788726Z INFO property_map_server::pocketbase: PocketBase collection 'invites' already exists
2026-02-19T22:01:29.788728Z INFO property_map_server::pocketbase: PocketBase collection 'short_urls' already exists

View file

@ -19,7 +19,7 @@ pub const AI_FILTERS_TEMPERATURE: f32 = 0.0;
/// Inner London free zone bounds (south, west, north, east) — roughly zones 12.
/// Users without a license can only query data within these bounds.
pub const FREE_ZONE_BOUNDS: (f64, f64, f64, f64) = (51.48, -0.18, 51.54, -0.02);
pub const FREE_ZONE_BOUNDS: (f64, f64, f64, f64) = (51.42, -0.34, 51.60, 0.14);
/// Homepage demo center (lat, lng). Unlicensed hexagon requests are allowed
/// when the center of the requested bounds is within DEMO_CENTER_TOLERANCE of this point.

View file

@ -437,82 +437,11 @@ impl PropertyData {
tracing::info!(rows = prop_count, "Properties joined with postcodes");
// Load online listings (buy + rent) — these have their own lat/lon.
// Normalize column names from finder output to server-expected names.
// strict=false: columns already using the new name are silently skipped.
// Expects the new finder parquet format with human-readable column names.
let load_listings = |path: &Path, label: &str| -> anyhow::Result<DataFrame> {
tracing::info!("Loading {} listings from {:?}", label, path);
let mut lf = LazyFrame::scan_parquet(path, Default::default())
let lf = LazyFrame::scan_parquet(path, Default::default())
.with_context(|| format!("Failed to scan {label} listings parquet"))?;
let schema = lf
.collect_schema()
.with_context(|| format!("Failed to read {label} listings schema"))?;
// Rename raw finder columns → server-expected names (no-op if already renamed)
let lf = lf.rename(
[
"postcode",
"address",
"latitude",
"longitude",
"bedrooms",
"bathrooms",
"total_rooms",
"tenure",
"property_type",
"property_sub_type",
"price_qualifier",
"floorspace_sqm",
"url",
"features",
],
[
"Postcode",
"Address per Property Register",
"lat",
"lon",
"Bedrooms",
"Bathrooms",
"Number of bedrooms & living rooms",
"Leashold/Freehold",
"Property type",
"Property sub-type",
"Price qualifier",
"Total floor area (sqm)",
"Listing URL",
"Listing features",
],
false,
);
// Derive missing columns for raw finder output that doesn't have them
let listing_status = if label == "buy" {
"For sale"
} else {
"For rent"
};
let lf = if schema.get("Listing status").is_none() {
lf.with_column(lit(listing_status).alias("Listing status"))
} else {
lf
};
let lf = if schema.get("Asking price").is_none() && schema.get("price").is_some() {
if label == "buy" {
lf.with_column(col("price").alias("Asking price"))
} else {
// Normalize rent to monthly: weekly×52/12, yearly÷12
lf.with_column(
when(col("price_frequency").eq(lit("weekly")))
.then(col("price").cast(DataType::Float64) * lit(52.0 / 12.0))
.when(col("price_frequency").eq(lit("yearly")))
.then(col("price").cast(DataType::Float64) / lit(12.0))
.otherwise(col("price").cast(DataType::Float64))
.cast(DataType::Int64)
.alias("Asking rent (monthly)"),
)
}
} else {
lf
};
// Join with postcodes for area features (listings have their own lat/lon)
let pc_no_coords = postcode_df.clone().lazy().drop(["lat", "lon"]);

View file

@ -478,7 +478,7 @@ async fn main() -> anyhow::Result<()> {
)
.route(
"/api/export",
get(move |ext, query| routes::get_export(state_export.clone(), ext, query))
get(move |headers, ext, query| routes::get_export(state_export.clone(), headers, ext, query))
.layer(ConcurrencyLimitLayer::new(3)),
)
.route("/api/me", get(routes::get_me))
@ -592,6 +592,16 @@ async fn main() -> anyhow::Result<()> {
.layer(CompressionLayer::new().zstd(true).gzip(true))
.layer(TraceLayer::new_for_http());
// Lock all current and future memory pages to prevent swapping
unsafe {
if libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) != 0 {
let err = std::io::Error::last_os_error();
tracing::warn!("mlockall failed (need CAP_IPC_LOCK or sufficient RLIMIT_MEMLOCK): {err}");
} else {
info!("All memory pages locked (mlockall)");
}
}
let addr = consts::SERVER_ADDRESS;
let listener = tokio::net::TcpListener::bind(addr)
.await

View file

@ -11,6 +11,7 @@ use crate::state::AppState;
const OG_PLACEHOLDER: &str = r#"<meta name="x-og-placeholder" content="__PERFECT_POSTCODE_OG_TAGS__"/>"#;
pub async fn og_middleware(request: Request, next: Next) -> Response {
let path = request.uri().path().to_string();
// Capture the query string before passing the request through
let query_string = request.uri().query().unwrap_or("").to_string();
@ -19,6 +20,11 @@ pub async fn og_middleware(request: Request, next: Next) -> Response {
let response = next.run(request).await;
// Only inject OG tags into SPA HTML responses, not proxied PocketBase responses
if path.starts_with("/pb/") || path.starts_with("/api/") {
return response;
}
let content_type = response
.headers()
.get(header::CONTENT_TYPE)

View file

@ -1,6 +1,6 @@
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tracing::{info, warn};
use tracing::info;
#[derive(Deserialize)]
struct AuthResponse {
@ -432,29 +432,25 @@ pub async fn ensure_oauth_providers(
// Update OAuth2 providers
let providers = settings
.pointer_mut("/oauth2/providers")
.and_then(|v| v.as_array_mut());
.and_then(|v| v.as_array_mut())
.ok_or_else(|| anyhow::anyhow!("PocketBase settings missing oauth2.providers array — cannot configure OAuth"))?;
if let Some(providers) = providers {
for provider in providers.iter_mut() {
let name = provider
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("");
match name {
"google" => {
provider["clientId"] = serde_json::json!(google_client_id);
provider["clientSecret"] = serde_json::json!(google_client_secret);
provider["enabled"] = serde_json::json!(true);
info!("Configured Google OAuth provider");
}
_ => {}
}
let google = match providers
.iter()
.position(|p| p.get("name").and_then(|n| n.as_str()) == Some("google"))
{
Some(idx) => &mut providers[idx],
None => {
info!("Google provider not found in PocketBase settings — adding it");
providers.push(serde_json::json!({"name": "google"}));
providers.last_mut().expect("just pushed")
}
} else {
warn!("PocketBase settings missing oauth2.providers array — cannot configure OAuth");
return Ok(());
}
};
google["clientId"] = serde_json::json!(google_client_id);
google["clientSecret"] = serde_json::json!(google_client_secret);
google["enabled"] = serde_json::json!(true);
info!("Configured Google OAuth provider");
// PATCH settings back
let patch_resp = client

View file

@ -37,7 +37,7 @@ pub use pois::{get_poi_categories, get_pois};
pub use postcode_stats::get_postcode_stats;
pub use postcodes::{get_postcode_lookup, get_postcodes};
pub use properties::get_hexagon_properties;
pub use screenshot::get_screenshot;
pub use screenshot::{fetch_screenshot_bytes, get_screenshot};
pub use shorten::{get_short_url, post_shorten};
pub use streetview::get_streetview;
pub use invites::{get_invite, post_invites, post_redeem_invite};

View file

@ -40,10 +40,10 @@ pub fn build_ollama_schema(_features: &FeaturesResponse) -> Value {
"type": "object",
"properties": {
"name": { "type": "string" },
"min": { "type": "number" },
"max": { "type": "number" }
"bound": { "type": "string", "enum": ["min", "max"] },
"value": { "type": "number" }
},
"required": ["name"]
"required": ["name", "bound", "value"]
}
},
"enum_filters": {
@ -80,11 +80,11 @@ pub fn build_system_prompt(features: &FeaturesResponse) -> String {
Rules:\n\
- ONLY set filters the user explicitly mentioned or clearly implied.\n\
- Leave out any filter the user did not mention. Empty arrays are fine.\n\
- For numeric filters, omit \"min\" to leave the lower bound open, \
omit \"max\" to leave the upper bound open.\n\
- Each numeric filter sets ONE bound only: \"min\" (at least this value) \
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 crime features. \
- \"low crime\" / \"safe\" = low values on Serious crime and Minor crime summary features. \
\"quiet\" = low Noise (dB). \"green\" / \"near parks\" = high Number of parks within 2km.\n\
- When the user says a number like \"under 400k\", interpret it as 400000.\n\
- When the user says \"3 bed\" or \"3 bedroom\", use Number of bedrooms & living rooms \
@ -98,6 +98,10 @@ pub fn build_system_prompt(features: &FeaturesResponse) -> String {
// Feature catalogue
parts.push("\n--- AVAILABLE FEATURES ---\n".to_string());
for group in &features.groups {
// Skip individual crime features — only expose "Crime summary" aggregates
if group.name == "Crime" {
continue;
}
parts.push(format!("## {}", group.name));
for feature in &group.features {
match feature {
@ -141,7 +145,7 @@ pub fn build_system_prompt(features: &FeaturesResponse) -> String {
parts.push(
"User: \"cheap freehold house under 400k\"\n\
Output: {\"numeric_filters\": [{\"name\": \"Last known price\", \"max\": 400000}], \
Output: {\"numeric_filters\": [{\"name\": \"Last known price\", \"bound\": \"max\", \"value\": 400000}], \
\"enum_filters\": [{\"name\": \"Leashold/Freehold\", \"values\": [\"Freehold\"]}, \
{\"name\": \"Property type\", \"values\": [\"Detached\", \"Semi-Detached\", \"Terraced\"]}], \
\"notes\": \"\"}"
@ -151,12 +155,12 @@ pub fn build_system_prompt(features: &FeaturesResponse) -> String {
parts.push(
"\nUser: \"safe quiet area with good schools and parks\"\n\
Output: {\"numeric_filters\": [\
{\"name\": \"Violence and sexual offences (avg/yr)\", \"max\": 20}, \
{\"name\": \"Burglary (avg/yr)\", \"max\": 10}, \
{\"name\": \"Noise (dB)\", \"max\": 55}, \
{\"name\": \"Good+ primary schools within 5km\", \"min\": 5}, \
{\"name\": \"Good+ secondary schools within 5km\", \"min\": 2}, \
{\"name\": \"Number of parks within 2km\", \"min\": 3}], \
{\"name\": \"Serious crime (avg/yr)\", \"bound\": \"max\", \"value\": 20}, \
{\"name\": \"Minor crime (avg/yr)\", \"bound\": \"max\", \"value\": 50}, \
{\"name\": \"Noise (dB)\", \"bound\": \"max\", \"value\": 55}, \
{\"name\": \"Good+ primary schools within 5km\", \"bound\": \"min\", \"value\": 5}, \
{\"name\": \"Good+ secondary schools within 5km\", \"bound\": \"min\", \"value\": 2}, \
{\"name\": \"Number of parks within 2km\", \"bound\": \"min\", \"value\": 3}], \
\"enum_filters\": [], \"notes\": \"\"}"
.to_string(),
);
@ -164,9 +168,9 @@ pub fn build_system_prompt(features: &FeaturesResponse) -> String {
parts.push(
"\nUser: \"3 bed flat under 300k with fast broadband near the beach\"\n\
Output: {\"numeric_filters\": [\
{\"name\": \"Last known price\", \"max\": 300000}, \
{\"name\": \"Number of bedrooms & living rooms\", \"min\": 4}, \
{\"name\": \"Max available download speed (Mbps)\", \"min\": 100}], \
{\"name\": \"Last known price\", \"bound\": \"max\", \"value\": 300000}, \
{\"name\": \"Number of bedrooms & living rooms\", \"bound\": \"min\", \"value\": 4}, \
{\"name\": \"Max available download speed (Mbps)\", \"bound\": \"min\", \"value\": 100}], \
\"enum_filters\": [{\"name\": \"Property type\", \"values\": [\"Flat\"]}], \
\"notes\": \"No filter for: beach proximity\"}"
.to_string(),
@ -175,9 +179,9 @@ pub fn build_system_prompt(features: &FeaturesResponse) -> String {
parts.push(
"\nUser: \"large family home with a garden near restaurants\"\n\
Output: {\"numeric_filters\": [\
{\"name\": \"Total floor area (sqm)\", \"min\": 100}, \
{\"name\": \"Number of bedrooms & living rooms\", \"min\": 5}, \
{\"name\": \"Number of restaurants within 2km\", \"min\": 10}], \
{\"name\": \"Total floor area (sqm)\", \"bound\": \"min\", \"value\": 100}, \
{\"name\": \"Number of bedrooms & living rooms\", \"bound\": \"min\", \"value\": 5}, \
{\"name\": \"Number of restaurants within 2km\", \"bound\": \"min\", \"value\": 10}], \
\"enum_filters\": [{\"name\": \"Property type\", \
\"values\": [\"Detached\", \"Semi-Detached\"]}], \
\"notes\": \"No filter for: garden\"}"
@ -187,7 +191,7 @@ pub fn build_system_prompt(features: &FeaturesResponse) -> String {
// Output format reminder
parts.push(
"\n--- OUTPUT FORMAT ---\n\
{\"numeric_filters\": [...], \"enum_filters\": [...], \"notes\": \"...\"}\n\
{\"numeric_filters\": [{\"name\": \"...\", \"bound\": \"min\"|\"max\", \"value\": N}, ...], \"enum_filters\": [...], \"notes\": \"...\"}\n\
Respond with ONLY the JSON object. No explanation."
.to_string(),
);
@ -244,10 +248,10 @@ pub async fn post_ai_filters(
/// Validate LLM output against feature metadata and convert to FeatureFilters format.
///
/// Input format (array-based, grammar-friendly):
/// Input format (array-based, each numeric filter sets one bound):
/// ```json
/// {
/// "numeric_filters": [{"name": "Last known price", "min": 0, "max": 300000}],
/// "numeric_filters": [{"name": "Last known price", "bound": "max", "value": 300000}],
/// "enum_filters": [{"name": "Leashold/Freehold", "values": ["Freehold"]}]
/// }
/// ```
@ -278,7 +282,7 @@ fn validate_and_convert(raw: &Value, features: &FeaturesResponse) -> Value {
}
}
// Process numeric filters
// Process numeric filters — each sets one bound (min or max)
if let Some(arr) = raw.get("numeric_filters").and_then(|val| val.as_array()) {
for item in arr {
let name = match item.get("name").and_then(|val| val.as_str()) {
@ -289,16 +293,19 @@ fn validate_and_convert(raw: &Value, features: &FeaturesResponse) -> Value {
Some(range) => *range,
None => continue,
};
let filter_min = item
.get("min")
.and_then(|val| val.as_f64())
.map(|num| num.max(feat_min as f64).min(feat_max as f64) as f32)
.unwrap_or(feat_min);
let filter_max = item
.get("max")
.and_then(|val| val.as_f64())
.map(|num| num.max(feat_min as f64).min(feat_max as f64) as f32)
.unwrap_or(feat_max);
let bound = match item.get("bound").and_then(|val| val.as_str()) {
Some(b) => b,
None => continue,
};
let value = match item.get("value").and_then(|val| val.as_f64()) {
Some(v) => v.max(feat_min as f64).min(feat_max as f64) as f32,
None => continue,
};
let (filter_min, filter_max) = match bound {
"min" => (value, feat_max),
"max" => (feat_min, value),
_ => continue,
};
// Only include if range is narrower than full range
if filter_min > feat_min || filter_max < feat_max {
result.insert(name.to_string(), json!([filter_min, filter_max]));

View file

@ -3,7 +3,7 @@ use std::hash::{Hash, Hasher};
use std::sync::Arc;
use axum::extract::Query;
use axum::http::{header, StatusCode};
use axum::http::{header, HeaderMap, StatusCode};
use axum::response::IntoResponse;
use axum::Extension;
use rust_xlsxwriter::{Format, FormatAlign, FormatBorder, Image, Url, Workbook};
@ -14,7 +14,7 @@ use tracing::{info, warn};
use crate::auth::OptionalUser;
use crate::licensing::check_license_bounds;
use crate::parsing::{parse_field_indices, parse_filters, require_bounds, row_passes_filters};
use crate::routes::FeatureInfo;
use crate::routes::{fetch_screenshot_bytes, FeatureInfo};
use crate::state::AppState;
const MAX_EXPORT_POSTCODES: usize = 250;
@ -120,39 +120,9 @@ fn build_frontend_params(
parts.join("&")
}
/// Fetch a screenshot image from the screenshot service for Excel export.
async fn fetch_screenshot(
state: &AppState,
frontend_params: &str,
) -> Option<Vec<u8>> {
let screenshot_base = &state.screenshot_url;
let url = format!("{}/screenshot?{}", screenshot_base, frontend_params);
match state.http_client.get(&url).send().await {
Ok(resp) if resp.status().is_success() => match resp.bytes().await {
Ok(bytes) => {
info!(bytes = bytes.len(), "Fetched screenshot for export");
Some(bytes.to_vec())
}
Err(err) => {
warn!("Failed to read screenshot response for export: {err}");
None
}
},
Ok(resp) => {
warn!(status = %resp.status(), "Screenshot service returned error for export");
None
}
Err(err) => {
warn!("Failed to reach screenshot service for export: {err}");
None
}
}
}
pub async fn get_export(
state: Arc<AppState>,
headers: HeaderMap,
Extension(user): Extension<OptionalUser>,
Query(params): Query<ExportParams>,
) -> Result<impl IntoResponse, axum::response::Response> {
@ -186,7 +156,18 @@ pub async fn get_export(
build_frontend_params(center_lat, center_lon, zoom, filters_str.as_deref());
// Fetch screenshot (async, before spawn_blocking)
let screenshot_bytes = fetch_screenshot(&state, &frontend_params).await;
let auth_header = headers.get(header::AUTHORIZATION);
let screenshot_bytes = match fetch_screenshot_bytes(&state, &frontend_params, auth_header).await
{
Ok(bytes) => {
info!(bytes = bytes.len(), "Fetched screenshot for export");
Some(bytes)
}
Err(err) => {
warn!("Screenshot failed for export: {err}");
None
}
};
// Build feature name → description map from the precomputed features response
let feature_descriptions: FxHashMap<String, String> = state

View file

@ -1,4 +1,5 @@
use std::sync::Arc;
use std::sync::{Arc, LazyLock};
use std::time::Duration;
use axum::body::Body;
use axum::extract::Request;
@ -8,6 +9,17 @@ use tracing::warn;
use crate::state::AppState;
/// Dedicated HTTP client for proxying — does not follow redirects so 3xx
/// responses are passed through to the browser (needed for OAuth flows).
static PROXY_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(5))
.build()
.expect("Failed to build proxy HTTP client")
});
pub async fn proxy_to_pocketbase(state: Arc<AppState>, req: Request) -> impl IntoResponse {
let pb_url = state.pocketbase_url.trim_end_matches('/');
@ -21,7 +33,7 @@ pub async fn proxy_to_pocketbase(state: Arc<AppState>, req: Request) -> impl Int
let url = format!("{pb_url}{target_path}{query}");
let method = req.method().clone();
let mut builder = state.http_client.request(method, &url);
let mut builder = PROXY_CLIENT.request(method, &url);
// Forward only safe headers (allowlist)
const ALLOWED_HEADERS: &[&str] = &[
@ -37,6 +49,21 @@ pub async fn proxy_to_pocketbase(state: Arc<AppState>, req: Request) -> impl Int
}
}
// Forward client IP so PocketBase rate-limits per-user, not per-server.
// Prefer existing X-Forwarded-For (from reverse proxy), fall back to X-Real-IP.
if let Some(xff) = req.headers().get("x-forwarded-for") {
builder = builder.header("X-Forwarded-For", xff.clone());
// First IP in the chain is the original client
if let Ok(s) = xff.to_str() {
if let Some(client_ip) = s.split(',').next().map(str::trim) {
builder = builder.header("X-Real-IP", client_ip);
}
}
} else if let Some(real_ip) = req.headers().get("x-real-ip") {
builder = builder.header("X-Forwarded-For", real_ip.clone());
builder = builder.header("X-Real-IP", real_ip.clone());
}
// Forward body
let body_bytes = match axum::body::to_bytes(req.into_body(), 10 * 1024 * 1024).await {
Ok(bytes) => bytes,

View file

@ -1,55 +1,65 @@
use std::sync::Arc;
use axum::http::header::HeaderValue;
use axum::http::{header, HeaderMap, StatusCode, Uri};
use axum::response::IntoResponse;
use tracing::{info, warn};
use crate::state::AppState;
/// Fetch a PNG screenshot from the screenshot service.
/// Used by both the `/api/screenshot` proxy and the xlsx export.
pub async fn fetch_screenshot_bytes(
state: &AppState,
query_string: &str,
auth_header: Option<&HeaderValue>,
) -> Result<Vec<u8>, String> {
let url = format!("{}/screenshot?{}", state.screenshot_url, query_string);
info!("Fetching screenshot from: {}", url);
let mut req = state.http_client.get(&url);
if let Some(auth) = auth_header {
req = req.header(header::AUTHORIZATION, auth);
}
match req.send().await {
Ok(resp) if resp.status().is_success() => resp
.bytes()
.await
.map(|b| b.to_vec())
.map_err(|err| format!("Failed to read screenshot response: {err}")),
Ok(resp) => {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
Err(format!(
"Screenshot service returned {status}: {body}"
))
}
Err(err) => Err(format!("Failed to reach screenshot service: {err}")),
}
}
pub async fn get_screenshot(
state: Arc<AppState>,
headers: HeaderMap,
uri: Uri,
) -> impl IntoResponse {
let screenshot_base = &state.screenshot_url;
let qs = uri.query().unwrap_or_default();
let auth = headers.get(header::AUTHORIZATION);
let qs = uri
.query()
.map(|q| format!("?{q}"))
.unwrap_or_default();
let url = format!("{screenshot_base}/screenshot{qs}");
info!("Proxying screenshot request to: {}", url);
let mut req = state.http_client.get(&url);
if let Some(auth) = headers.get(header::AUTHORIZATION) {
req = req.header(header::AUTHORIZATION, auth);
}
match req.send().await {
Ok(resp) if resp.status().is_success() => match resp.bytes().await {
Ok(bytes) => (
StatusCode::OK,
[
(header::CONTENT_TYPE, "image/png"),
(header::CACHE_CONTROL, "public, max-age=86400"),
],
bytes,
)
.into_response(),
Err(err) => {
warn!("Failed to read screenshot response: {}", err);
(StatusCode::BAD_GATEWAY, "Failed to read screenshot").into_response()
}
},
Ok(resp) => {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
warn!("Screenshot service returned status {}: {}", status, body);
(StatusCode::BAD_GATEWAY, "Screenshot service error").into_response()
}
match fetch_screenshot_bytes(&state, qs, auth).await {
Ok(bytes) => (
StatusCode::OK,
[
(header::CONTENT_TYPE, "image/png"),
(header::CACHE_CONTROL, "public, max-age=86400"),
],
bytes,
)
.into_response(),
Err(err) => {
warn!("Failed to reach screenshot service: {}", err);
(StatusCode::BAD_GATEWAY, "Screenshot service unavailable").into_response()
warn!("{err}");
(StatusCode::BAD_GATEWAY, "Screenshot service error").into_response()
}
}
}