From 787428f1a5caa8bb072cbc6bf7a58f83d77768ea Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Thu, 19 Feb 2026 22:24:06 +0000 Subject: [PATCH] Deploy again --- .dockerignore | 4 + Dockerfile | 14 +- docker-compose.yml | 20 +- frontend/src/App.tsx | 2 +- frontend/src/lib/consts.ts | 2 +- server-rs/Cargo.lock | 1 + server-rs/Cargo.toml | 1 + server-rs/logs/server.log.2026-02-19 | 522 +++++++++++++++++++++++++++ server-rs/src/consts.rs | 2 +- server-rs/src/data/property.rs | 75 +--- server-rs/src/main.rs | 12 +- server-rs/src/og_middleware.rs | 6 + server-rs/src/pocketbase.rs | 40 +- server-rs/src/routes.rs | 2 +- server-rs/src/routes/ai_filters.rs | 73 ++-- server-rs/src/routes/export.rs | 49 +-- server-rs/src/routes/pb_proxy.rs | 31 +- server-rs/src/routes/screenshot.rs | 84 +++-- 18 files changed, 717 insertions(+), 223 deletions(-) create mode 100644 server-rs/logs/server.log.2026-02-19 diff --git a/.dockerignore b/.dockerignore index d340b4f..4832cf7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,7 @@ .venv **/node_modules **/dist +r5-java server-rs/target .git .task @@ -8,3 +9,6 @@ server-rs/target __pycache__ analyses/ *.log +property-data +manual-data +!property-data/arcgis_data.parquet diff --git a/Dockerfile b/Dockerfile index b2394c1..335a228 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,12 +21,12 @@ WORKDIR /app COPY --from=server /app/server-rs/target/release/property-map-server ./ COPY --from=frontend /app/frontend/dist ./frontend/dist/ -COPY property-data/wide.parquet ./data/ -COPY property-data/filtered_uk_pois.parquet ./data/ -COPY property-data/places.parquet ./data/ -COPY property-data/uk.pmtiles ./data/ -COPY manual-data/postcode_boundaries ./data/postcode_boundaries/ -COPY property-data/travel-times ./data/travel-times/ +# COPY property-data/wide.parquet ./data/ +# COPY property-data/filtered_uk_pois.parquet ./data/ +# COPY property-data/places.parquet ./data/ +# COPY property-data/uk.pmtiles ./data/ +# COPY manual-data/postcode_boundaries ./data/postcode_boundaries/ +# COPY property-data/travel-times ./data/travel-times/ RUN chown -R appuser:appuser /app USER appuser @@ -34,4 +34,4 @@ EXPOSE 8001 HEALTHCHECK --interval=30s --timeout=5s --start-period=120s --retries=3 \ CMD curl -f http://localhost:8001/health || exit 1 ENTRYPOINT ["./property-map-server"] -CMD ["--data", "/app/data/wide.parquet", "--pois", "/app/data/filtered_uk_pois.parquet", "--places", "/app/data/places.parquet", "--tiles", "/app/data/uk.pmtiles", "--postcodes", "/app/data/postcode_boundaries", "--travel-times", "/app/data/travel-times", "--dist", "/app/frontend/dist"] +CMD ["--properties", "/app/data/properties.parquet", "--postcode-features", "/app/data/postcode.parquet", "--listings-buy", "/app/data-scraped/online_listings_buy.parquet", "--listings-rent", "/app/data-scraped/online_listings_rent.parquet", "--pois", "/app/data/filtered_uk_pois.parquet", "--places", "/app/data/places.parquet", "--tiles", "/app/data/uk.pmtiles", "--postcodes", "/app/data/postcode_boundaries", "--travel-times", "/app/data/travel-times", "--dist", "/app/frontend/dist"] diff --git a/docker-compose.yml b/docker-compose.yml index 8ebd434..5211f46 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,20 +10,27 @@ services: command: > bash -c " cargo install cargo-watch && - cargo watch -i logs/ -x 'run -- --properties /app/data/properties.parquet --postcode-features /app/data/postcode.parquet --listings-buy /app/data/online_listings_buy.parquet --listings-rent /app/data/online_listings_rent.parquet --pois /app/data/filtered_uk_pois.parquet --places /app/data/places.parquet --tiles /app/data/uk.pmtiles --postcodes /app/data/postcode_boundaries --travel-times /app/data/travel-times' + cargo watch -i logs/ -x 'run -- --properties /app/data/properties.parquet --postcode-features /app/data/postcode.parquet --listings-buy /app/data-scraped/online_listings_buy.parquet --listings-rent /app/data-scraped/online_listings_rent.parquet --pois /app/data/filtered_uk_pois.parquet --places /app/data/places.parquet --tiles /app/data/uk.pmtiles --postcodes /app/data/postcode_boundaries --travel-times /app/data/travel-times' " ports: - "8001:8001" networks: - dev-network + cap_add: + - IPC_LOCK + ulimits: + memlock: + soft: -1 + hard: -1 extra_hosts: - "host.docker.internal:host-gateway" volumes: - .:/app - - cargo-registry:/usr/local/cargo/registry + - cargo-home:/usr/local/cargo - cargo-target:/app/server-rs/target - ./property-data:/app/data:ro - ./property-data/travel-times:/app/data/travel-times:ro + - /volumes/narrowit/property-data/scraped:/app/data-scraped:ro environment: POCKETBASE_URL: http://pocketbase:8090 POCKETBASE_ADMIN_EMAIL: *pb-email @@ -60,13 +67,6 @@ services: timeout: 5s retries: 3 start_period: 30s - deploy: - resources: - reservations: - devices: - - driver: nvidia - capabilities: [ gpu ] - count: 1 frontend: init: true @@ -151,7 +151,7 @@ services: volumes: pb-data: - cargo-registry: + cargo-home: cargo-target: frontend-node-modules: screenshot-cache: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5c02cc0..b8d58db 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -302,7 +302,7 @@ export default function App() { searchesLoading={savedSearches.loading} onDeleteSearch={savedSearches.deleteSearch} onOpenSearch={(params) => { - window.location.href = `/?${params}`; + window.location.href = `/dashboard?${params}`; }} /> ) : activePage === 'invite' && inviteCode ? ( diff --git a/frontend/src/lib/consts.ts b/frontend/src/lib/consts.ts index 362ba1f..2625075 100644 --- a/frontend/src/lib/consts.ts +++ b/frontend/src/lib/consts.ts @@ -14,7 +14,7 @@ export const MAP_MIN_ZOOM = 5.5; export const BUFFER_MULTIPLIER = 1.5; /** Inner London free zone bounds (south, west, north, east) — must match server FREE_ZONE_BOUNDS */ -export const FREE_ZONE_BOUNDS = { south: 51.48, west: -0.18, north: 51.54, east: -0.02 }; +export const FREE_ZONE_BOUNDS = { south: 51.42, west: -0.34, north: 51.60, east: 0.14 }; export const INITIAL_VIEW_STATE: ViewState = { longitude: (FREE_ZONE_BOUNDS.west + FREE_ZONE_BOUNDS.east) / 2, diff --git a/server-rs/Cargo.lock b/server-rs/Cargo.lock index a266c39..d0c8390 100644 --- a/server-rs/Cargo.lock +++ b/server-rs/Cargo.lock @@ -2450,6 +2450,7 @@ dependencies = [ "hex", "hmac", "lasso", + "libc", "metrics", "metrics-exporter-prometheus", "parking_lot", diff --git a/server-rs/Cargo.toml b/server-rs/Cargo.toml index d859c59..fa897bd 100644 --- a/server-rs/Cargo.toml +++ b/server-rs/Cargo.toml @@ -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" diff --git a/server-rs/logs/server.log.2026-02-19 b/server-rs/logs/server.log.2026-02-19 new file mode 100644 index 0000000..22dedda --- /dev/null +++ b/server-rs/logs/server.log.2026-02-19 @@ -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 diff --git a/server-rs/src/consts.rs b/server-rs/src/consts.rs index 7d66930..57d6c4e 100644 --- a/server-rs/src/consts.rs +++ b/server-rs/src/consts.rs @@ -19,7 +19,7 @@ pub const AI_FILTERS_TEMPERATURE: f32 = 0.0; /// Inner London free zone bounds (south, west, north, east) — roughly zones 1–2. /// 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. diff --git a/server-rs/src/data/property.rs b/server-rs/src/data/property.rs index baccdfd..d60564f 100644 --- a/server-rs/src/data/property.rs +++ b/server-rs/src/data/property.rs @@ -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 { 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"]); diff --git a/server-rs/src/main.rs b/server-rs/src/main.rs index fa8ee79..4b52d91 100644 --- a/server-rs/src/main.rs +++ b/server-rs/src/main.rs @@ -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 diff --git a/server-rs/src/og_middleware.rs b/server-rs/src/og_middleware.rs index dfd4b9f..c5a424a 100644 --- a/server-rs/src/og_middleware.rs +++ b/server-rs/src/og_middleware.rs @@ -11,6 +11,7 @@ use crate::state::AppState; const OG_PLACEHOLDER: &str = r#""#; 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) diff --git a/server-rs/src/pocketbase.rs b/server-rs/src/pocketbase.rs index 152ea39..7afb7d1 100644 --- a/server-rs/src/pocketbase.rs +++ b/server-rs/src/pocketbase.rs @@ -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 diff --git a/server-rs/src/routes.rs b/server-rs/src/routes.rs index d727656..6089973 100644 --- a/server-rs/src/routes.rs +++ b/server-rs/src/routes.rs @@ -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}; diff --git a/server-rs/src/routes/ai_filters.rs b/server-rs/src/routes/ai_filters.rs index e1884ae..3f0534a 100644 --- a/server-rs/src/routes/ai_filters.rs +++ b/server-rs/src/routes/ai_filters.rs @@ -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])); diff --git a/server-rs/src/routes/export.rs b/server-rs/src/routes/export.rs index 6e3e440..706e3ea 100644 --- a/server-rs/src/routes/export.rs +++ b/server-rs/src/routes/export.rs @@ -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> { - 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, + headers: HeaderMap, Extension(user): Extension, Query(params): Query, ) -> Result { @@ -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 = state diff --git a/server-rs/src/routes/pb_proxy.rs b/server-rs/src/routes/pb_proxy.rs index 9cb473c..122507d 100644 --- a/server-rs/src/routes/pb_proxy.rs +++ b/server-rs/src/routes/pb_proxy.rs @@ -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 = 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, 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, 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, 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, diff --git a/server-rs/src/routes/screenshot.rs b/server-rs/src/routes/screenshot.rs index 4556e42..9404958 100644 --- a/server-rs/src/routes/screenshot.rs +++ b/server-rs/src/routes/screenshot.rs @@ -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, 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, 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() } } }