seems fine

This commit is contained in:
Andras Schmelczer 2026-05-05 22:29:28 +01:00
parent 48983e3b4b
commit 7a1696541f
37 changed files with 4999 additions and 1242 deletions

View file

@ -1618,3 +1618,973 @@
2026-05-04T21:01:19.544562Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:01:19.556893Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:01:19.558130Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:07:10.355869Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=64311 parallel=true cells_before_filter=249 cells_after_filter=231 truncated=false bounds=51.4958,-0.1636,51.5342,-0.0964 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.8 json_ms=0.1 total_ms=0.9
2026-05-04T21:07:29.438731Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:07:30.930722Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:07:30.932184Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:07:32.691536Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.7 json_ms=0.4 total_ms=2.0
2026-05-04T21:07:51.398221Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=52 cells_after_filter=37 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=4 filters_raw="Estimated current price:0:600000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.8 json_ms=0.0 total_ms=1.8
2026-05-04T21:07:51.649551Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=152340 filters=4 travel=0 total=192 filters_raw="Estimated current price:0:600000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" ms=3.0
2026-05-04T21:07:55.810502Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=392 cells_after_filter=297 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=3 filters_raw="Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=2.6 json_ms=0.4 total_ms=3.0
2026-05-04T21:07:57.323367Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=52 cells_after_filter=37 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=4 filters_raw="Estimated current price:0:600000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.7 json_ms=0.1 total_ms=1.8
2026-05-04T21:08:28.517850Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=261 cells_after_filter=182 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=4 filters_raw="Estimated current price:12976.594:1380000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.5 json_ms=0.2 total_ms=2.7
2026-05-04T21:08:28.518933Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=152340 filters=4 travel=0 total=2266 filters_raw="Estimated current price:12976.594:1380000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" ms=3.7
2026-05-04T21:08:38.632679Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=89194ad325bffff resolution=9 total_count=1 filters=4 filters_raw="Estimated current price:12976.594:1380000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" ms=0.1
2026-05-04T21:08:39.686104Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=108397 parallel=true cells_before_filter=183 cells_after_filter=134 truncated=false bounds=51.4943,-0.1671,51.5357,-0.0929 filters=4 filters_raw="Estimated current price:12976.594:1380000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=3.2 json_ms=0.2 total_ms=3.3
2026-05-04T21:08:39.710272Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=108397 filters=4 travel=0 total=1583 filters_raw="Estimated current price:12976.594:1380000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" ms=2.3
2026-05-04T21:10:59.962261Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:10:59.962264Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:11:29.998393Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:11:29.998405Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:11:30.044607Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:11:30.045945Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:11:52.906468Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:11:52.907285Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:11:53.875945Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:11:53.900406Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:12:02.485327Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:12:02.485341Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:12:02.871760Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:12:02.873319Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:12:15.721128Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:12:15.721131Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:12:15.873635Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:12:15.873891Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:13:41.600447Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:13:41.600448Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:14:10.905771Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:14:10.906312Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:14:10.917360Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:14:10.926781Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:14:14.273968Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:14:14.274980Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:14:14.288306Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:14:14.289553Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:16:30.344962Z INFO property_map_server: Prometheus metrics initialized
2026-05-04T21:16:30.345109Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet
2026-05-04T21:16:30.345114Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-05-04T21:16:30.426500Z INFO property_map_server::data::property: Postcode features loaded rows=1263786
2026-05-04T21:16:30.426510Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-05-04T21:16:33.162256Z INFO property_map_server::data::property: Properties joined with postcodes rows=15268176
2026-05-04T21:16:33.162293Z INFO property_map_server::data::property: Feature columns from config numeric=63 enums=6 total=69
2026-05-04T21:16:36.375023Z WARN property_map_server::data::property: Dropped properties with missing postcode coordinates rows=743076
2026-05-04T21:16:36.375034Z INFO property_map_server::data::property: Combined data selected rows=14525100
2026-05-04T21:16:36.504029Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-05-04T21:16:36.905967Z INFO property_map_server::data::property: Computing histograms for numeric features
2026-05-04T21:16:38.095951Z INFO property_map_server::data::property: Extracting string columns
2026-05-04T21:16:39.478199Z INFO property_map_server::data::property: Building enum features
2026-05-04T21:16:40.751598Z INFO property_map_server::data::property: Extracting renovation history
2026-05-04T21:16:45.902894Z INFO property_map_server: Prometheus metrics initialized
2026-05-04T21:16:45.903095Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet
2026-05-04T21:16:45.903117Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-05-04T21:16:45.975537Z INFO property_map_server::data::property: Postcode features loaded rows=1263786
2026-05-04T21:16:45.975550Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-05-04T21:16:48.852633Z INFO property_map_server::data::property: Properties joined with postcodes rows=15268176
2026-05-04T21:16:48.852686Z INFO property_map_server::data::property: Feature columns from config numeric=63 enums=6 total=69
2026-05-04T21:16:51.727345Z WARN property_map_server::data::property: Dropped properties with missing postcode coordinates rows=743076
2026-05-04T21:16:51.727354Z INFO property_map_server::data::property: Combined data selected rows=14525100
2026-05-04T21:16:51.853922Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-05-04T21:16:52.259794Z INFO property_map_server::data::property: Computing histograms for numeric features
2026-05-04T21:16:53.519465Z INFO property_map_server::data::property: Extracting string columns
2026-05-04T21:16:54.902888Z INFO property_map_server::data::property: Building enum features
2026-05-04T21:16:56.099583Z INFO property_map_server::data::property: Extracting renovation history
2026-05-04T21:16:58.328466Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1741939
2026-05-04T21:16:58.328474Z INFO property_map_server::data::property: Sorting rows by spatial locality
2026-05-04T21:16:59.302215Z INFO property_map_server::data::property: Building interned strings
2026-05-04T21:17:05.078928Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted, quantized to u16)
2026-05-04T21:17:13.299855Z INFO property_map_server: Prometheus metrics initialized
2026-05-04T21:17:13.300056Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet
2026-05-04T21:17:13.300070Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-05-04T21:17:13.388566Z INFO property_map_server::data::property: Postcode features loaded rows=1263786
2026-05-04T21:17:13.388576Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-05-04T21:17:16.112850Z INFO property_map_server::data::property: Properties joined with postcodes rows=15268176
2026-05-04T21:17:16.112896Z INFO property_map_server::data::property: Feature columns from config numeric=63 enums=6 total=69
2026-05-04T21:17:18.943990Z WARN property_map_server::data::property: Dropped properties with missing postcode coordinates rows=743076
2026-05-04T21:17:18.944000Z INFO property_map_server::data::property: Combined data selected rows=14525100
2026-05-04T21:17:19.074139Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-05-04T21:17:19.478164Z INFO property_map_server::data::property: Computing histograms for numeric features
2026-05-04T21:17:20.900871Z INFO property_map_server::data::property: Extracting string columns
2026-05-04T21:17:22.270617Z INFO property_map_server::data::property: Building enum features
2026-05-04T21:17:23.464560Z INFO property_map_server::data::property: Extracting renovation history
2026-05-04T21:17:25.627267Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1741939
2026-05-04T21:17:25.627275Z INFO property_map_server::data::property: Sorting rows by spatial locality
2026-05-04T21:17:26.514957Z INFO property_map_server::data::property: Building interned strings
2026-05-04T21:17:32.201475Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted, quantized to u16)
2026-05-04T21:17:39.253903Z INFO property_map_server::data::property: Data loading complete
2026-05-04T21:17:40.705499Z INFO property_map_server: Allocator trim label="property data load" trimmed=true rss_before_mib=12930.5 rss_after_mib=3177.9 released_mib=9752.6
2026-05-04T21:17:40.705511Z INFO property_map_server: Property data loaded rows=14525100 features=69 enums=6
2026-05-04T21:17:40.705515Z INFO property_map_server: Building spatial grid index (0.01° cells)
2026-05-04T21:17:40.809720Z INFO property_map_server: Precomputing H3 cells at resolution 12
2026-05-04T21:17:40.809729Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
2026-05-04T21:17:41.203126Z INFO property_map_server::data::property: H3 precomputation complete (14525100 cells)
2026-05-04T21:17:41.203149Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
2026-05-04T21:17:41.203156Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
2026-05-04T21:17:41.231149Z INFO property_map_server::data::poi: Loaded 567534 POIs
2026-05-04T21:17:41.381403Z INFO property_map_server::data::poi: POI string columns interned category_unique=94 group_unique=11 emoji_unique=71
2026-05-04T21:17:41.382547Z INFO property_map_server::data::poi: POI data loading complete.
2026-05-04T21:17:41.429667Z INFO property_map_server: Allocator trim label="poi data load" trimmed=true rss_before_mib=3584.4 rss_after_mib=3395.2 released_mib=189.1
2026-05-04T21:17:41.429679Z INFO property_map_server: POI data loaded pois=567534
2026-05-04T21:17:41.429682Z INFO property_map_server: Building POI spatial grid index
2026-05-04T21:17:41.437592Z INFO property_map_server: Loading place data from /app/data/places.parquet
2026-05-04T21:17:41.437604Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
2026-05-04T21:17:41.438262Z INFO property_map_server::data::places: Loaded 3474 places
2026-05-04T21:17:41.439221Z INFO property_map_server::data::places: Place data loaded places=3474 types=2 with_population=71 with_city=3392
2026-05-04T21:17:41.443763Z INFO property_map_server: Allocator trim label="place data load" trimmed=true rss_before_mib=3404.3 rss_after_mib=3399.9 released_mib=4.4
2026-05-04T21:17:41.443773Z INFO property_map_server: Place data loaded places=3474
2026-05-04T21:17:41.443782Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
2026-05-04T21:17:41.443794Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
2026-05-04T21:17:41.469434Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
2026-05-04T21:17:50.535745Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
2026-05-04T21:17:50.911994Z INFO property_map_server: Allocator trim label="postcode boundary load" trimmed=true rss_before_mib=10725.6 rss_after_mib=10526.1 released_mib=199.6
2026-05-04T21:17:50.912004Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
2026-05-04T21:17:51.067150Z INFO property_map_server::data::postcodes: Outcode data derived from postcodes outcodes=2361
2026-05-04T21:17:51.067221Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
2026-05-04T21:17:51.067464Z INFO property_map_server: PMTiles loaded successfully
2026-05-04T21:17:51.106375Z INFO property_map_server: No --dist provided; static serving and OG injection disabled
2026-05-04T21:17:51.136547Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
2026-05-04T21:17:51.136731Z INFO property_map_server: Precomputed features response groups=8
2026-05-04T21:17:51.136746Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
2026-05-04T21:17:51.212590Z INFO property_map_server::pocketbase: PocketBase users collection already has all required fields
2026-05-04T21:17:51.515914Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
2026-05-04T21:17:51.519337Z INFO property_map_server::pocketbase: PocketBase collection 'saved_properties' API rules updated
2026-05-04T21:18:27.672179Z INFO property_map_server: Prometheus metrics initialized
2026-05-04T21:18:27.672342Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet
2026-05-04T21:18:27.672350Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-05-04T21:18:27.738364Z INFO property_map_server::data::property: Postcode features loaded rows=1263786
2026-05-04T21:18:27.738378Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-05-04T21:18:30.329460Z INFO property_map_server::data::property: Properties joined with postcodes rows=15268176
2026-05-04T21:18:30.329500Z INFO property_map_server::data::property: Feature columns from config numeric=63 enums=6 total=69
2026-05-04T21:18:32.935500Z WARN property_map_server::data::property: Dropped properties with missing postcode coordinates rows=743076
2026-05-04T21:18:32.935509Z INFO property_map_server::data::property: Combined data selected rows=14525100
2026-05-04T21:18:33.057215Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-05-04T21:18:33.411442Z INFO property_map_server::data::property: Computing histograms for numeric features
2026-05-04T21:18:34.615065Z INFO property_map_server::data::property: Extracting string columns
2026-05-04T21:18:35.946034Z INFO property_map_server::data::property: Building enum features
2026-05-04T21:18:37.161827Z INFO property_map_server::data::property: Extracting renovation history
2026-05-04T21:18:39.203014Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1741939
2026-05-04T21:18:39.203022Z INFO property_map_server::data::property: Sorting rows by spatial locality
2026-05-04T21:18:40.060146Z INFO property_map_server::data::property: Building interned strings
2026-05-04T21:19:00.964738Z INFO property_map_server: Prometheus metrics initialized
2026-05-04T21:19:00.964898Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet
2026-05-04T21:19:00.964909Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-05-04T21:19:01.045232Z INFO property_map_server::data::property: Postcode features loaded rows=1263786
2026-05-04T21:19:01.045241Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-05-04T21:19:03.683522Z INFO property_map_server::data::property: Properties joined with postcodes rows=15268176
2026-05-04T21:19:03.683554Z INFO property_map_server::data::property: Feature columns from config numeric=63 enums=6 total=69
2026-05-04T21:19:06.678528Z WARN property_map_server::data::property: Dropped properties with missing postcode coordinates rows=743076
2026-05-04T21:19:06.678539Z INFO property_map_server::data::property: Combined data selected rows=14525100
2026-05-04T21:19:06.820178Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-05-04T21:19:07.223089Z INFO property_map_server::data::property: Computing histograms for numeric features
2026-05-04T21:19:08.782981Z INFO property_map_server::data::property: Extracting string columns
2026-05-04T21:19:10.395522Z INFO property_map_server::data::property: Building enum features
2026-05-04T21:19:11.637823Z INFO property_map_server::data::property: Extracting renovation history
2026-05-04T21:19:13.851485Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1741939
2026-05-04T21:19:13.851495Z INFO property_map_server::data::property: Sorting rows by spatial locality
2026-05-04T21:19:14.789536Z INFO property_map_server::data::property: Building interned strings
2026-05-04T21:19:20.543430Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted, quantized to u16)
2026-05-04T21:19:23.642436Z INFO property_map_server::data::property: Data loading complete
2026-05-04T21:19:25.249114Z INFO property_map_server: Allocator trim label="property data load" trimmed=true rss_before_mib=12859.7 rss_after_mib=3226.5 released_mib=9633.2
2026-05-04T21:19:25.249127Z INFO property_map_server: Property data loaded rows=14525100 features=69 enums=6
2026-05-04T21:19:25.249130Z INFO property_map_server: Building spatial grid index (0.01° cells)
2026-05-04T21:19:25.355938Z INFO property_map_server: Precomputing H3 cells at resolution 12
2026-05-04T21:19:25.355947Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
2026-05-04T21:19:25.761535Z INFO property_map_server::data::property: H3 precomputation complete (14525100 cells)
2026-05-04T21:19:25.761575Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
2026-05-04T21:19:25.761589Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
2026-05-04T21:19:25.788010Z INFO property_map_server::data::poi: Loaded 567534 POIs
2026-05-04T21:19:25.941519Z INFO property_map_server::data::poi: POI string columns interned category_unique=94 group_unique=11 emoji_unique=71
2026-05-04T21:19:25.942891Z INFO property_map_server::data::poi: POI data loading complete.
2026-05-04T21:19:25.989855Z INFO property_map_server: Allocator trim label="poi data load" trimmed=true rss_before_mib=3634.0 rss_after_mib=3444.1 released_mib=189.9
2026-05-04T21:19:25.989867Z INFO property_map_server: POI data loaded pois=567534
2026-05-04T21:19:25.989870Z INFO property_map_server: Building POI spatial grid index
2026-05-04T21:19:25.998237Z INFO property_map_server: Loading place data from /app/data/places.parquet
2026-05-04T21:19:25.998254Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
2026-05-04T21:19:25.998909Z INFO property_map_server::data::places: Loaded 3474 places
2026-05-04T21:19:25.999902Z INFO property_map_server::data::places: Place data loaded places=3474 types=2 with_population=71 with_city=3392
2026-05-04T21:19:26.003577Z INFO property_map_server: Allocator trim label="place data load" trimmed=true rss_before_mib=3453.3 rss_after_mib=3448.8 released_mib=4.5
2026-05-04T21:19:26.003589Z INFO property_map_server: Place data loaded places=3474
2026-05-04T21:19:26.003600Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
2026-05-04T21:19:26.003611Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
2026-05-04T21:19:26.006554Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
2026-05-04T21:19:33.580078Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
2026-05-04T21:19:33.940360Z INFO property_map_server: Allocator trim label="postcode boundary load" trimmed=true rss_before_mib=10756.3 rss_after_mib=10557.9 released_mib=198.4
2026-05-04T21:19:33.940372Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
2026-05-04T21:19:34.095945Z INFO property_map_server::data::postcodes: Outcode data derived from postcodes outcodes=2361
2026-05-04T21:19:34.095996Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
2026-05-04T21:19:34.149362Z INFO property_map_server: PMTiles loaded successfully
2026-05-04T21:19:34.188674Z INFO property_map_server: No --dist provided; static serving disabled
2026-05-04T21:19:34.220411Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
2026-05-04T21:19:34.220659Z INFO property_map_server: Precomputed features response groups=8
2026-05-04T21:19:34.220676Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
2026-05-04T21:19:34.272045Z INFO property_map_server::pocketbase: PocketBase users collection already has all required fields
2026-05-04T21:19:34.276489Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
2026-05-04T21:19:34.281445Z INFO property_map_server::pocketbase: PocketBase collection 'saved_properties' API rules updated
2026-05-04T21:19:34.364449Z INFO property_map_server::pocketbase: PocketBase meta.appURL set to https://perfect-postcodes.co.uk/pb
2026-05-04T21:19:34.372757Z INFO property_map_server::pocketbase: PocketBase OAuth configured on users collection
2026-05-04T21:19:34.372842Z INFO property_map_server: Gemini configured (model: gemini-3-flash-preview)
2026-05-04T21:19:34.372864Z INFO property_map_server: Loading travel time data from /app/data/travel-times
2026-05-04T21:19:34.382336Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=2753
2026-05-04T21:19:34.389548Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=2753
2026-05-04T21:19:34.399662Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="car" destinations=2753
2026-05-04T21:19:34.408881Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=2752
2026-05-04T21:19:34.408917Z INFO property_map_server: Travel time store loaded modes=4
2026-05-04T21:19:34.408966Z INFO property_map_server: Precomputed AI filters system prompt
2026-05-04T21:19:44.689686Z INFO property_map_server: All memory pages locked (mlockall)
2026-05-04T21:19:44.689720Z INFO property_map_server: Server listening on 0.0.0.0:8001
2026-05-04T21:19:45.521046Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:19:46.703870Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:19:46.705139Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:19:48.457712Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:19:48.459328Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:19:49.862812Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:19:49.864269Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:19:50.686286Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=11.5 json_ms=0.3 total_ms=11.8
2026-05-04T21:20:08.572854Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=52 cells_after_filter=37 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=4 filters_raw="Estimated current price:0:600000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=3.1 json_ms=0.1 total_ms=3.2
2026-05-04T21:20:08.820170Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=152340 filters=4 travel=0 total=192 filters_raw="Estimated current price:0:600000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" ms=3.0
2026-05-04T21:20:13.123166Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=392 cells_after_filter=297 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=3 filters_raw="Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=3.8 json_ms=0.5 total_ms=4.4
2026-05-04T21:20:15.004875Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=52 cells_after_filter=37 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=4 filters_raw="Estimated current price:0:600000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=2.2 json_ms=0.1 total_ms=2.3
2026-05-04T21:20:43.935003Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=152340 filters=4 travel=0 total=2266 filters_raw="Estimated current price:12976.594:1380000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" ms=2.9
2026-05-04T21:20:43.940349Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=261 cells_after_filter=182 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=4 filters_raw="Estimated current price:12976.594:1380000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=8.0 json_ms=0.2 total_ms=8.2
2026-05-04T21:20:53.152011Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=89194ad325bffff resolution=9 total_count=1 filters=4 filters_raw="Estimated current price:12976.594:1380000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" ms=0.1
2026-05-04T21:20:53.886934Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=108397 parallel=true cells_before_filter=183 cells_after_filter=134 truncated=false bounds=51.4943,-0.1671,51.5357,-0.0929 filters=4 filters_raw="Estimated current price:12976.594:1380000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.9 json_ms=0.1 total_ms=1.0
2026-05-04T21:20:54.311995Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=108397 filters=4 travel=0 total=1583 filters_raw="Estimated current price:12976.594:1380000;;Number of bedrooms & living rooms:4:6;;Property type:Detached|Semi-Detached|Terraced;;Distance to nearest train or tube station (km):0:1" ms=3.4
2026-05-04T21:23:14.002548Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:23:14.002553Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:23:14.885571Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:23:14.885899Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:28:24.898600Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:28:24.898612Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:28:40.888196Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:28:40.888204Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:30:09.864228Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:30:09.867843Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:30:09.879658Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:30:09.879680Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:33:13.462061Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:33:16.976720Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:33:57.226080Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.6
2026-05-04T21:34:03.822735Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:34:03.824308Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:34:24.874278Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:34:24.879329Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:34:48.816750Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:34:53.723428Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:34:53.723676Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:34:53.903706Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:34:53.903714Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:35:21.885930Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:35:21.889745Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:35:28.878329Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:35:28.878578Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:36:03.824012Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:36:03.824207Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:36:03.894459Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:36:03.894603Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:36:03.896824Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:36:03.900692Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:37:35.949585Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:37:35.950905Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:37:50.747341Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:37:50.757709Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:37:50.875450Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:37:50.881785Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:37:50.881790Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:37:50.882098Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:38:45.015175Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:38:45.015179Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:38:45.896227Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:38:45.901008Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:39:03.876874Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:39:03.877337Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:39:09.027276Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:39:09.035644Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:39:10.034456Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:39:10.034459Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:39:37.942331Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:39:37.942548Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:39:38.905987Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:39:38.909982Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:40:36.876309Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:40:36.876510Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:40:39.913835Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:40:39.915358Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:40:39.915444Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:40:39.919463Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:40:40.032873Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:40:40.037649Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:41:03.975071Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:41:03.975346Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:41:04.913909Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:41:04.919871Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:42:54.181539Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:42:55.702312Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:42:55.703626Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:42:56.110756Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=3.8 json_ms=0.3 total_ms=4.1
2026-05-04T21:43:10.163996Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.7
2026-05-04T21:43:10.184457Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.7
2026-05-04T21:43:10.582333Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=182.8 agg_ms=1.3 json_ms=0.0 total_ms=184.2
2026-05-04T21:43:10.673220Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.2
2026-05-04T21:43:19.396620Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.6 json_ms=0.1 total_ms=1.7
2026-05-04T21:43:20.185266Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.9 json_ms=0.1 total_ms=3.0
2026-05-04T21:43:32.458294Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.3 json_ms=0.0 total_ms=1.4
2026-05-04T21:43:32.731139Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.7
2026-05-04T21:43:47.913139Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:43:47.915273Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:43:47.918535Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:43:47.922605Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:43:47.930124Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:43:47.937684Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:44:02.476219Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:44:02.481022Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:44:02.635976Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.6
2026-05-04T21:44:02.888032Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0400 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.8 json_ms=0.0 total_ms=2.9
2026-05-04T21:44:03.777803Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.8
2026-05-04T21:44:04.332655Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0400 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.9 json_ms=0.1 total_ms=1.9
2026-05-04T21:44:04.579701Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.7
2026-05-04T21:45:31.271440Z INFO property_map_server::routes::pois: GET /api/pois results=2 candidates=106 categories=1 categories_raw="Bakery" ms=0.0
2026-05-04T21:45:31.271748Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=5749 parallel=false cells_before_filter=205 cells_after_filter=193 truncated=false bounds=52.4771,0.8684,52.5473,1.0384 filters=2 filters_raw="Outstanding primary schools within 5km:0:13;;Outstanding secondary schools within 5km:0:4" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.2 json_ms=0.1 total_ms=0.3
2026-05-04T21:45:31.521188Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=5749 filters=2 travel=0 total=5749 filters_raw="Outstanding primary schools within 5km:0:13;;Outstanding secondary schools within 5km:0:4" ms=0.1
2026-05-04T21:45:45.569748Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:45:45.569757Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:45:46.085245Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:45:46.085435Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:46:27.779500Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:46:27.781022Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:46:27.895780Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:46:27.895880Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:46:27.895962Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:46:27.896163Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:46:31.868072Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:46:31.868078Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:46:31.887443Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:46:31.888421Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:46:32.880541Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:46:32.881908Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:46:32.895414Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:46:32.897862Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:47:53.098117Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:47:53.099184Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:47:53.881339Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:47:53.883980Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:47:53.893566Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:47:53.893593Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:47:53.896287Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:47:53.897850Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:49:06.869789Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:49:06.874076Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:49:06.890936Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:49:06.893924Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:49:06.911752Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:49:06.912758Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:49:06.925912Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:49:06.927142Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:49:45.400194Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:49:46.755368Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:49:46.756851Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:49:47.134339Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=3.0 json_ms=0.3 total_ms=3.3
2026-05-04T21:50:01.449061Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.5
2026-05-04T21:50:01.477122Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.8
2026-05-04T21:50:01.699749Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.9 json_ms=0.1 total_ms=1.9
2026-05-04T21:50:02.021720Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.2
2026-05-04T21:50:08.430182Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=0.9 json_ms=0.1 total_ms=0.9
2026-05-04T21:50:10.874075Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.2 json_ms=0.1 total_ms=1.3
2026-05-04T21:50:19.601888Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:50:19.603766Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:50:19.632454Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:50:19.632632Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:50:19.896460Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:50:19.898391Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:50:19.903939Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:50:19.905530Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:50:19.908881Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:50:19.913126Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:50:20.428655Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.9
2026-05-04T21:50:20.434364Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=6.7 json_ms=0.1 total_ms=6.8
2026-05-04T21:50:20.468217Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.9 json_ms=0.1 total_ms=3.0
2026-05-04T21:50:22.089671Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.9 json_ms=0.1 total_ms=2.0
2026-05-04T21:50:22.090509Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.8
2026-05-04T21:50:40.042107Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:50:40.042113Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:50:48.042644Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.2 total_ms=1.5
2026-05-04T21:50:49.345436Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=1214362 parallel=true cells_before_filter=5967 cells_after_filter=5802 truncated=false bounds=51.4334,-0.3126,51.6124,0.1116 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=11.3 json_ms=3.6 total_ms=15.0
2026-05-04T21:50:50.135421Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=2442848 parallel=true cells_before_filter=3898 cells_after_filter=3890 truncated=false bounds=51.3375,-0.5231,51.7322,0.4127 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=10.8 json_ms=1.9 total_ms=12.8
2026-05-04T21:50:51.354636Z INFO property_map_server::routes::postcodes: GET /api/postcodes postcodes_before_filter=1725 postcodes_after_filter=219 filtered_out=1506 truncated=false bounds=51.510465,-0.143230,51.515758,-0.130685 filters=0 filters_raw="-" fields=0 travel_entries=0 agg_ms=1.3 json_ms=0.7 total_ms=1.9
2026-05-04T21:50:51.941610Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=40248 parallel=false cells_before_filter=186 cells_after_filter=150 truncated=false bounds=51.5023,-0.1612,51.5260,-0.1049 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.7 json_ms=0.1 total_ms=0.8
2026-05-04T21:50:53.589925Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=40248 parallel=false cells_before_filter=186 cells_after_filter=150 truncated=false bounds=51.5023,-0.1612,51.5260,-0.1049 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.1 json_ms=0.1 total_ms=1.3
2026-05-04T21:50:57.385975Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=89195da4987ffff resolution=9 total_count=244 filters=0 filters_raw="-" ms=0.3
2026-05-04T21:50:57.735827Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=40248 parallel=false cells_before_filter=186 cells_after_filter=122 truncated=false bounds=51.5023,-0.1536,51.5260,-0.1126 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.0 json_ms=0.1 total_ms=1.2
2026-05-04T21:51:36.199186Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=40248 parallel=false cells_before_filter=186 cells_after_filter=122 truncated=false bounds=51.5023,-0.1536,51.5260,-0.1126 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.9 json_ms=0.1 total_ms=1.0
2026-05-04T21:51:40.435673Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:51:40.436199Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:51:40.436621Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:51:40.439811Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:51:40.451919Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:51:40.451948Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:51:40.461983Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:51:40.462001Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:51:40.756926Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.8 json_ms=0.2 total_ms=2.0
2026-05-04T21:51:41.685644Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:51:41.686016Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:51:41.690808Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:51:41.690945Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:51:41.708144Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:51:41.708177Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:51:41.719164Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:51:41.719177Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:51:41.998854Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.0 json_ms=0.2 total_ms=1.2
2026-05-04T21:52:20.289809Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:52:20.291154Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:52:20.475111Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.2 json_ms=0.2 total_ms=1.4
2026-05-04T21:52:20.890339Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:52:20.890372Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:52:20.898898Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:52:20.902649Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:52:20.907465Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:52:20.907472Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:52:58.073984Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:52:58.076101Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:52:58.249258Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.0 json_ms=0.2 total_ms=1.2
2026-05-04T21:52:58.889010Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:52:58.889757Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:52:58.898342Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:52:58.902657Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:52:58.902725Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:52:58.907164Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:53:35.425317Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:53:35.425338Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:53:35.613332Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.4 json_ms=0.2 total_ms=1.6
2026-05-04T21:53:35.883634Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:53:35.883704Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:53:35.894889Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:53:35.901771Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:53:35.902111Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:53:35.902628Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:55:46.371570Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:55:46.373081Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:55:46.553405Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.2 json_ms=0.2 total_ms=1.4
2026-05-04T21:55:46.891885Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:55:46.893094Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:55:46.903844Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:55:46.906495Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:55:46.906576Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:55:46.915610Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:55:57.716037Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:56:00.226058Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:56:00.227285Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:56:00.576766Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.5 json_ms=0.3 total_ms=1.8
2026-05-04T21:56:10.593840Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:56:10.594012Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:56:10.611505Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:56:10.611527Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:56:10.765236Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.8 json_ms=0.4 total_ms=2.2
2026-05-04T21:56:10.797375Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.7 json_ms=0.3 total_ms=3.0
2026-05-04T21:56:10.910205Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:56:10.910231Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:56:10.916559Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:56:10.921383Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:56:10.925627Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:56:10.925657Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:56:14.846854Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.9
2026-05-04T21:56:14.847537Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.6
2026-05-04T21:56:14.853045Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=14 cells_after_filter=9 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=7.0 json_ms=0.0 total_ms=7.0
2026-05-04T21:56:15.343655Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.0 json_ms=0.1 total_ms=2.1
2026-05-04T21:56:15.356716Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.2
2026-05-04T21:56:21.467200Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.5 json_ms=0.1 total_ms=1.5
2026-05-04T21:56:21.805660Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.0 json_ms=0.1 total_ms=1.0
2026-05-04T21:56:40.924937Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.7 json_ms=0.1 total_ms=1.7
2026-05-04T21:56:41.209756Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.6
2026-05-04T21:56:52.609815Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=79996 parallel=true cells_before_filter=14 cells_after_filter=12 truncated=false bounds=51.4966,-0.1310,51.5308,-0.0491 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.9 json_ms=0.0 total_ms=1.9
2026-05-04T21:56:53.571886Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=72082 parallel=true cells_before_filter=7 cells_after_filter=7 truncated=false bounds=51.5001,-0.1244,51.5284,-0.0568 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=0.8 json_ms=0.0 total_ms=0.9
2026-05-04T21:56:53.894537Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=72082 filters=5 travel=1 total=11 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=1.3
2026-05-04T21:56:54.926593Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:56:54.926607Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:56:55.104320Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.9 json_ms=0.2 total_ms=2.1
2026-05-04T21:56:55.889693Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:56:55.889752Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:56:55.905455Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:56:55.905628Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:56:55.914181Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:56:55.920824Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:56:58.704315Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:56:58.704320Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:56:58.872821Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=72082 parallel=true cells_before_filter=7 cells_after_filter=7 truncated=false bounds=51.5001,-0.1244,51.5284,-0.0568 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=0.8 json_ms=0.0 total_ms=0.8
2026-05-04T21:56:59.121385Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=72082 filters=5 travel=1 total=11 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=1.0
2026-05-04T21:57:56.775802Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:57:56.777300Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:57:56.885572Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:57:56.886980Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:57:56.898376Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:57:56.898392Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:57:56.903289Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:57:56.905401Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:57:56.946078Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.2 total_ms=1.5
2026-05-04T21:58:10.729026Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:58:12.148740Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:58:12.151290Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:58:12.523672Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.2 json_ms=0.3 total_ms=2.5
2026-05-04T21:58:27.003322Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.7
2026-05-04T21:58:27.003894Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.4
2026-05-04T21:58:27.004089Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=14 cells_after_filter=9 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.1 json_ms=0.0 total_ms=1.1
2026-05-04T21:58:27.008726Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=152340 filters=5 travel=1 total=25 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.4
2026-05-04T21:58:27.069148Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:58:27.070840Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:58:27.089231Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:58:27.089647Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:58:27.249044Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.3 json_ms=0.3 total_ms=2.6
2026-05-04T21:58:27.325395Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.5 json_ms=0.0 total_ms=1.6
2026-05-04T21:58:27.580214Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.9
2026-05-04T21:58:27.888907Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:58:27.892491Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:58:27.900029Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:58:27.901909Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:58:27.903069Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:58:27.906552Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:58:33.559096Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.5 json_ms=0.1 total_ms=2.5
2026-05-04T21:58:35.972273Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.0 json_ms=0.1 total_ms=2.0
2026-05-04T21:58:47.839690Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.0 json_ms=0.0 total_ms=1.1
2026-05-04T21:58:48.120664Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.2
2026-05-04T21:58:56.219997Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=72082 parallel=true cells_before_filter=7 cells_after_filter=7 truncated=false bounds=51.4984,-0.1275,51.5295,-0.0532 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.8 json_ms=0.0 total_ms=2.8
2026-05-04T21:58:56.518264Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=72082 parallel=true cells_before_filter=7 cells_after_filter=7 truncated=false bounds=51.5001,-0.1244,51.5284,-0.0568 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.7 json_ms=0.0 total_ms=1.7
2026-05-04T21:59:00.346359Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=72082 filters=5 travel=1 total=11 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=1.4
2026-05-04T21:59:54.147033Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:59:54.148019Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:59:54.166292Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:59:54.168495Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:59:54.181021Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:59:54.184809Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:59:54.194227Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T21:59:54.197244Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T21:59:54.585195Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.0 json_ms=0.2 total_ms=1.2
2026-05-04T22:00:14.301897Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:00:15.630685Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:00:15.630884Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:00:15.964464Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.0 json_ms=0.3 total_ms=2.3
2026-05-04T22:00:24.946312Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:00:24.946320Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:00:24.946412Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:00:24.952388Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:00:30.022362Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.2
2026-05-04T22:00:30.043112Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.9
2026-05-04T22:00:30.261451Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.3 json_ms=0.1 total_ms=1.4
2026-05-04T22:00:30.592129Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.8
2026-05-04T22:00:37.007421Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.4 json_ms=0.0 total_ms=1.4
2026-05-04T22:00:37.462647Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.7 json_ms=0.1 total_ms=2.8
2026-05-04T22:00:45.840231Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.7 json_ms=0.0 total_ms=3.8
2026-05-04T22:00:46.316882Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.0
2026-05-04T22:00:57.236646Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=72082 parallel=true cells_before_filter=7 cells_after_filter=7 truncated=false bounds=51.5001,-0.1244,51.5284,-0.0568 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.6 json_ms=0.0 total_ms=1.7
2026-05-04T22:00:57.427349Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=72082 filters=5 travel=1 total=11 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=1.5
2026-05-04T22:02:03.462288Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:02:04.824984Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:02:04.826419Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:02:05.162248Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=3.7 json_ms=0.3 total_ms=4.1
2026-05-04T22:02:17.722889Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.7
2026-05-04T22:02:17.740905Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.0
2026-05-04T22:02:17.967131Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.5 json_ms=0.1 total_ms=1.6
2026-05-04T22:02:18.518303Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.2
2026-05-04T22:02:21.675085Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:02:21.675453Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:02:21.680135Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:02:21.680216Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:02:21.853343Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.5 json_ms=0.2 total_ms=1.7
2026-05-04T22:02:21.861053Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.3 json_ms=0.1 total_ms=1.4
2026-05-04T22:02:21.882614Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:02:21.884775Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:02:21.896884Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:02:21.896945Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:02:21.913545Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:02:21.920368Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:02:22.344366Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.9
2026-05-04T22:02:23.330984Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.3 json_ms=0.1 total_ms=3.4
2026-05-04T22:02:23.556756Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.8 json_ms=0.0 total_ms=1.8
2026-05-04T22:02:29.980499Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.8 json_ms=0.1 total_ms=1.8
2026-05-04T22:02:29.985702Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.9
2026-05-04T22:02:38.177070Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=72082 parallel=true cells_before_filter=7 cells_after_filter=7 truncated=false bounds=51.5001,-0.1244,51.5284,-0.0568 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.5 json_ms=0.0 total_ms=1.5
2026-05-04T22:02:38.177246Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=72082 filters=5 travel=1 total=11 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=1.6
2026-05-04T22:02:51.694677Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:02:51.694701Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:02:51.873538Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.2 json_ms=0.3 total_ms=2.4
2026-05-04T22:02:51.887968Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:02:51.889611Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:02:51.893970Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:02:51.897409Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:02:51.904091Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:02:51.904150Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:05:40.782606Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:05:40.784162Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:05:41.884454Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:05:41.885815Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:06:47.676019Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:06:48.950663Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:06:48.952417Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:06:49.286097Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.6 json_ms=0.3 total_ms=1.8
2026-05-04T22:07:01.856777Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.9
2026-05-04T22:07:01.875908Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.9
2026-05-04T22:07:02.096680Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.1 json_ms=0.1 total_ms=1.2
2026-05-04T22:07:02.387635Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.5
2026-05-04T22:07:07.234158Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.3 json_ms=0.1 total_ms=1.4
2026-05-04T22:07:07.780854Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.0 json_ms=0.1 total_ms=1.0
2026-05-04T22:07:09.771342Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:07:11.430730Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:07:11.432677Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:07:11.870399Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=5.2 json_ms=0.4 total_ms=5.6
2026-05-04T22:07:18.738335Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.6 json_ms=0.0 total_ms=1.6
2026-05-04T22:07:18.758383Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.2
2026-05-04T22:07:24.564862Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.0
2026-05-04T22:07:24.584804Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.5
2026-05-04T22:07:25.434415Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.6 json_ms=0.0 total_ms=1.7
2026-05-04T22:07:25.435484Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.8
2026-05-04T22:07:30.016548Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.4 json_ms=0.1 total_ms=2.5
2026-05-04T22:07:30.166842Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.9 json_ms=0.1 total_ms=2.0
2026-05-04T22:07:43.696232Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.8
2026-05-04T22:07:43.696849Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.5 json_ms=0.1 total_ms=3.5
2026-05-04T22:09:15.053435Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.9 json_ms=0.2 total_ms=1.0
2026-05-04T22:09:15.986390Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:09:15.986629Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:09:16.282846Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.9 json_ms=0.2 total_ms=1.1
2026-05-04T22:09:16.985235Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=89195da49c3ffff resolution=9 total_count=238 filters=0 filters_raw="-" ms=0.2
2026-05-04T22:09:17.396636Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=64311 parallel=true cells_before_filter=249 cells_after_filter=229 truncated=false bounds=51.4958,-0.1632,51.5342,-0.0968 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.9 json_ms=0.1 total_ms=1.0
2026-05-04T22:09:20.390663Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:09:21.638150Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:09:21.639834Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:09:21.968978Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=4.2 json_ms=0.4 total_ms=4.6
2026-05-04T22:09:34.546910Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.5
2026-05-04T22:09:34.564980Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4
2026-05-04T22:09:34.582175Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=89194ad3257ffff resolution=9 total_count=87 filters=0 filters_raw="-" ms=0.2
2026-05-04T22:09:34.796975Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.0 json_ms=0.1 total_ms=1.0
2026-05-04T22:09:35.096462Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.8
2026-05-04T22:09:35.791825Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=503301 parallel=true cells_before_filter=1899 cells_after_filter=1773 truncated=false bounds=51.4632,-0.2509,51.5723,-0.0624 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=5.4 json_ms=1.2 total_ms=6.6
2026-05-04T22:09:39.578502Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=3047 parallel=false cells_before_filter=155 cells_after_filter=134 truncated=false bounds=51.0079,-1.2718,51.0856,-1.0894 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.2 json_ms=0.1 total_ms=0.3
2026-05-04T22:09:39.932966Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.4 json_ms=0.1 total_ms=1.5
2026-05-04T22:09:40.921779Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.5 json_ms=0.0 total_ms=1.6
2026-05-04T22:09:48.386770Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.7 json_ms=0.0 total_ms=1.7
2026-05-04T22:09:49.833150Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.0
2026-05-04T22:09:59.625699Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:09:59.625804Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:10:18.276194Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:10:18.277375Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:10:50.331384Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:10:51.624480Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:10:51.624857Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:10:51.972591Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.7 json_ms=0.2 total_ms=1.9
2026-05-04T22:12:49.115326Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:12:59.972207Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:13:01.286073Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:13:01.287792Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:13:01.639675Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.0 json_ms=0.4 total_ms=2.4
2026-05-04T22:13:14.259373Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4
2026-05-04T22:13:14.279347Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.0
2026-05-04T22:13:14.524168Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.2 json_ms=0.1 total_ms=4.3
2026-05-04T22:13:14.856309Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.4
2026-05-04T22:13:32.500450Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:13:45.469478Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:13:58.113964Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:14:20.006022Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:15:16.034638Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:15:16.034655Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:15:16.049735Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:15:16.052310Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:15:16.067959Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:15:16.067966Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:15:23.544443Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:15:23.545908Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:15:24.793212Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:15:24.793390Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:15:38.499700Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:15:38.499937Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:15:38.888798Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:15:38.890273Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:15:38.901972Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:15:38.901980Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:15:43.175411Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:15:43.175417Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:15:52.910078Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:15:54.157456Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:15:54.157703Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:15:54.558417Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.3 json_ms=0.3 total_ms=2.6
2026-05-04T22:16:17.924667Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:16:23.301799Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:16:24.620993Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:16:24.622323Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:16:25.132503Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.2 json_ms=0.4 total_ms=2.6
2026-05-04T22:16:30.018214Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:16:30.021061Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:16:30.044102Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:16:30.045412Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:16:30.124918Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:16:30.126318Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:16:30.126371Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:16:30.130716Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:16:30.190264Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.2 json_ms=0.3 total_ms=2.5
2026-05-04T22:16:35.175651Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:16:35.790396Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:16:36.550571Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:16:36.550572Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:16:36.953085Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=5.3 json_ms=0.4 total_ms=5.7
2026-05-04T22:16:37.297770Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:16:37.297798Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:16:37.766664Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=10.7 json_ms=0.4 total_ms=11.1
2026-05-04T22:16:50.090443Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:16:50.714483Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.5
2026-05-04T22:16:50.737277Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.8
2026-05-04T22:16:50.988254Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.8 json_ms=0.0 total_ms=1.9
2026-05-04T22:16:51.380753Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.4
2026-05-04T22:16:51.728458Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:16:51.730984Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:16:52.544870Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:16:52.545173Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:16:53.170483Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.8 json_ms=0.3 total_ms=3.1
2026-05-04T22:16:56.117207Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.8 json_ms=0.1 total_ms=1.9
2026-05-04T22:16:58.078086Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.0 json_ms=0.1 total_ms=3.0
2026-05-04T22:17:05.602271Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.5
2026-05-04T22:17:05.627414Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.1
2026-05-04T22:17:06.887654Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.0 json_ms=0.1 total_ms=2.1
2026-05-04T22:17:06.891207Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.2
2026-05-04T22:17:11.374500Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=0.9 json_ms=0.1 total_ms=0.9
2026-05-04T22:17:11.787823Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.9 json_ms=0.1 total_ms=2.0
2026-05-04T22:17:12.104759Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.4 json_ms=0.1 total_ms=3.5
2026-05-04T22:17:12.614981Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.1
2026-05-04T22:17:16.298914Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:17:16.299362Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:17:16.652247Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:17:16.654687Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:17:16.887024Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:17:16.888739Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:17:16.895319Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:17:16.895348Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:17:16.937631Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.6 json_ms=0.1 total_ms=2.7
2026-05-04T22:17:17.394683Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.0 json_ms=0.1 total_ms=4.1
2026-05-04T22:17:17.395093Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=4.2
2026-05-04T22:17:18.394512Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:17:18.398180Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:17:18.736343Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.3 json_ms=0.1 total_ms=3.3
2026-05-04T22:17:19.156516Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.0
2026-05-04T22:17:20.079176Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=79996 parallel=true cells_before_filter=14 cells_after_filter=13 truncated=false bounds=51.4958,-0.1316,51.5310,-0.0473 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.1 json_ms=0.0 total_ms=1.1
2026-05-04T22:17:20.318654Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=79996 filters=5 travel=1 total=37 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=1.8
2026-05-04T22:17:21.165131Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.4 json_ms=0.1 total_ms=4.4
2026-05-04T22:17:22.568170Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.0
2026-05-04T22:17:23.643832Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:17:23.645798Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:17:23.676422Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:17:23.676427Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:17:23.883967Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:17:23.890107Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:17:23.901175Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:17:23.904281Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:17:26.512955Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.0 json_ms=0.0 total_ms=1.0
2026-05-04T22:17:26.514608Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.9
2026-05-04T22:17:28.082381Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:17:28.082407Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:17:28.591764Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=79996 parallel=true cells_before_filter=14 cells_after_filter=13 truncated=false bounds=51.4958,-0.1316,51.5310,-0.0473 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.0 json_ms=0.0 total_ms=1.1
2026-05-04T22:17:28.592307Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=79996 filters=5 travel=1 total=37 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=1.6
2026-05-04T22:17:30.502763Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=79996 parallel=true cells_before_filter=14 cells_after_filter=13 truncated=false bounds=51.4958,-0.1316,51.5310,-0.0473 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.0 json_ms=0.0 total_ms=1.0
2026-05-04T22:17:30.730121Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=79996 filters=5 travel=1 total=37 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=1.8
2026-05-04T22:17:56.113083Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:17:57.975715Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:17:57.975718Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:17:58.431515Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.6 json_ms=0.3 total_ms=1.9
2026-05-04T22:18:01.100097Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:18:01.100139Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:18:01.147582Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:18:01.147587Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:18:01.157499Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:18:01.158423Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:18:01.161998Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:18:01.164292Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:18:01.388520Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.7 json_ms=0.3 total_ms=2.0
2026-05-04T22:18:11.402161Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:18:12.712627Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:18:12.712664Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:18:13.073370Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.9 json_ms=0.4 total_ms=2.3
2026-05-04T22:18:30.994562Z INFO property_map_server: Prometheus metrics initialized
2026-05-04T22:18:30.994761Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet
2026-05-04T22:18:30.994774Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
2026-05-04T22:18:31.122555Z INFO property_map_server::data::property: Postcode features loaded rows=1263786
2026-05-04T22:18:31.122565Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
2026-05-04T22:18:33.611506Z INFO property_map_server::data::property: Properties joined with postcodes rows=15268176
2026-05-04T22:18:33.611537Z INFO property_map_server::data::property: Feature columns from config numeric=63 enums=6 total=69
2026-05-04T22:18:36.538024Z WARN property_map_server::data::property: Dropped properties with missing postcode coordinates rows=743076
2026-05-04T22:18:36.538033Z INFO property_map_server::data::property: Combined data selected rows=14525100
2026-05-04T22:18:36.678044Z INFO property_map_server::data::property: Extracting numeric feature columns
2026-05-04T22:18:37.038480Z INFO property_map_server::data::property: Computing histograms for numeric features
2026-05-04T22:18:38.526523Z INFO property_map_server::data::property: Extracting string columns
2026-05-04T22:18:39.908807Z INFO property_map_server::data::property: Building enum features
2026-05-04T22:18:41.719020Z INFO property_map_server::data::property: Extracting renovation history
2026-05-04T22:18:43.770345Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1741939
2026-05-04T22:18:43.770354Z INFO property_map_server::data::property: Sorting rows by spatial locality
2026-05-04T22:18:44.674520Z INFO property_map_server::data::property: Building interned strings
2026-05-04T22:18:50.148935Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted, quantized to u16)
2026-05-04T22:18:54.062453Z INFO property_map_server::data::property: Data loading complete
2026-05-04T22:18:55.930791Z INFO property_map_server: Allocator trim label="property data load" trimmed=true rss_before_mib=11749.0 rss_after_mib=3312.9 released_mib=8436.0
2026-05-04T22:18:55.930802Z INFO property_map_server: Property data loaded rows=14525100 features=69 enums=6
2026-05-04T22:18:55.930805Z INFO property_map_server: Building spatial grid index (0.01° cells)
2026-05-04T22:18:56.051230Z INFO property_map_server: Precomputing H3 cells at resolution 12
2026-05-04T22:18:56.051241Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
2026-05-04T22:18:56.475680Z INFO property_map_server::data::property: H3 precomputation complete (14525100 cells)
2026-05-04T22:18:56.475701Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
2026-05-04T22:18:56.475707Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
2026-05-04T22:18:56.506199Z INFO property_map_server::data::poi: Loaded 567534 POIs
2026-05-04T22:18:56.662262Z INFO property_map_server::data::poi: POI string columns interned category_unique=94 group_unique=11 emoji_unique=71
2026-05-04T22:18:56.663650Z INFO property_map_server::data::poi: POI data loading complete.
2026-05-04T22:18:56.710693Z INFO property_map_server: Allocator trim label="poi data load" trimmed=true rss_before_mib=3717.6 rss_after_mib=3530.6 released_mib=187.0
2026-05-04T22:18:56.710705Z INFO property_map_server: POI data loaded pois=567534
2026-05-04T22:18:56.710707Z INFO property_map_server: Building POI spatial grid index
2026-05-04T22:18:56.718782Z INFO property_map_server: Loading place data from /app/data/places.parquet
2026-05-04T22:18:56.718799Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
2026-05-04T22:18:56.720493Z INFO property_map_server::data::places: Loaded 3474 places
2026-05-04T22:18:56.721814Z INFO property_map_server::data::places: Place data loaded places=3474 types=2 with_population=71 with_city=3392
2026-05-04T22:18:56.725737Z INFO property_map_server: Allocator trim label="place data load" trimmed=true rss_before_mib=3539.8 rss_after_mib=3535.4 released_mib=4.4
2026-05-04T22:18:56.725748Z INFO property_map_server: Place data loaded places=3474
2026-05-04T22:18:56.725757Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
2026-05-04T22:18:56.725762Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
2026-05-04T22:18:56.727448Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
2026-05-04T22:19:05.022680Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
2026-05-04T22:19:05.421317Z INFO property_map_server: Allocator trim label="postcode boundary load" trimmed=true rss_before_mib=10855.9 rss_after_mib=10659.3 released_mib=196.5
2026-05-04T22:19:05.421335Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
2026-05-04T22:19:05.580884Z INFO property_map_server::data::postcodes: Outcode data derived from postcodes outcodes=2361
2026-05-04T22:19:05.580958Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
2026-05-04T22:19:05.581190Z INFO property_map_server: PMTiles loaded successfully
2026-05-04T22:19:05.620437Z INFO property_map_server: No --dist provided; static serving disabled
2026-05-04T22:19:05.653852Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
2026-05-04T22:19:05.654023Z INFO property_map_server: Precomputed features response groups=8
2026-05-04T22:19:05.654040Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
2026-05-04T22:19:05.712851Z INFO property_map_server::pocketbase: PocketBase users collection already has all required fields
2026-05-04T22:19:05.727511Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
2026-05-04T22:19:05.731610Z INFO property_map_server::pocketbase: PocketBase collection 'saved_properties' API rules updated
2026-05-04T22:19:07.308379Z INFO property_map_server::pocketbase: PocketBase meta.appURL set to https://perfect-postcodes.co.uk/pb
2026-05-04T22:19:07.315235Z INFO property_map_server::pocketbase: PocketBase OAuth configured on users collection
2026-05-04T22:19:07.315281Z INFO property_map_server: Gemini configured (model: gemini-3-flash-preview)
2026-05-04T22:19:07.315297Z INFO property_map_server: Loading travel time data from /app/data/travel-times
2026-05-04T22:19:07.317153Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=2753
2026-05-04T22:19:07.318775Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=2753
2026-05-04T22:19:07.320369Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="car" destinations=2753
2026-05-04T22:19:07.321670Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=2752
2026-05-04T22:19:07.321691Z INFO property_map_server: Travel time store loaded modes=4
2026-05-04T22:19:07.321753Z INFO property_map_server: Precomputed AI filters system prompt
2026-05-04T22:19:10.359858Z INFO property_map_server: All memory pages locked (mlockall)
2026-05-04T22:19:10.359896Z INFO property_map_server: Server listening on 0.0.0.0:8001
2026-05-04T22:19:10.362406Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:19:11.282862Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:19:11.998512Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:19:12.001101Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:19:12.651633Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:19:12.653579Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:19:12.847710Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=10.2 json_ms=0.3 total_ms=10.5
2026-05-04T22:19:13.876585Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:19:13.876992Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:19:14.398559Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=8.1 json_ms=0.4 total_ms=8.5
2026-05-04T22:19:14.829985Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:19:14.832130Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:19:17.878833Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:19:17.885215Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:19:17.885219Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:19:17.885248Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:19:27.189284Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.6
2026-05-04T22:19:27.207964Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.2
2026-05-04T22:19:27.673388Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=226.0 agg_ms=2.4 json_ms=0.1 total_ms=228.5
2026-05-04T22:19:27.698162Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.9
2026-05-04T22:19:27.838540Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.7
2026-05-04T22:19:27.860257Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.2
2026-05-04T22:19:28.165307Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.9 json_ms=0.1 total_ms=3.0
2026-05-04T22:19:28.463880Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.9
2026-05-04T22:19:32.718769Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.4 json_ms=0.0 total_ms=2.4
2026-05-04T22:19:32.890348Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.5 json_ms=0.1 total_ms=3.6
2026-05-04T22:19:33.342435Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.2 json_ms=0.1 total_ms=2.3
2026-05-04T22:19:33.562465Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.8 json_ms=0.1 total_ms=3.8
2026-05-04T22:19:47.613869Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.7
2026-05-04T22:19:47.615091Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.7 json_ms=0.0 total_ms=4.8
2026-05-04T22:19:49.409784Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.8 json_ms=0.0 total_ms=3.8
2026-05-04T22:19:49.410138Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=4.6
2026-05-04T22:19:56.769120Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=79996 parallel=true cells_before_filter=14 cells_after_filter=13 truncated=false bounds=51.4958,-0.1316,51.5310,-0.0473 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.7 json_ms=0.0 total_ms=3.8
2026-05-04T22:19:56.969498Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=79996 filters=5 travel=1 total=37 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.1
2026-05-04T22:19:57.508622Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=79996 parallel=true cells_before_filter=14 cells_after_filter=13 truncated=false bounds=51.4958,-0.1316,51.5310,-0.0473 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.8 json_ms=0.0 total_ms=2.8
2026-05-04T22:19:57.852562Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=79996 filters=5 travel=1 total=37 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.9
2026-05-04T22:20:54.025076Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:20:56.324428Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:20:56.324721Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:20:58.125348Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:20:58.130229Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:20:58.561997Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=3.4 json_ms=0.5 total_ms=3.9
2026-05-04T22:21:11.221943Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.6
2026-05-04T22:21:11.242460Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.6
2026-05-04T22:21:11.465642Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.7 json_ms=0.0 total_ms=1.8
2026-05-04T22:21:11.869894Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.1
2026-05-04T22:21:13.728615Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:21:13.728630Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:21:13.735932Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:21:13.738239Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:21:13.901248Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:21:13.902849Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:21:13.918576Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=6.7 json_ms=0.1 total_ms=6.7
2026-05-04T22:21:13.932881Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:21:13.932890Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:21:14.491621Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.8
2026-05-04T22:21:26.371953Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:21:26.371987Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:21:26.887586Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:21:26.889234Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:21:26.899961Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:21:26.904381Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:21:27.652641Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:21:27.652644Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:21:27.822545Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.0 json_ms=0.0 total_ms=1.0
2026-05-04T22:21:28.074620Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.6
2026-05-04T22:22:29.069537Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:22:29.071715Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:22:29.075423Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:22:29.075586Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:22:29.106537Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:22:29.106764Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:22:36.081084Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:22:36.082306Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:22:51.954249Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:22:51.954310Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:24:05.699059Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:24:07.592008Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:24:07.592432Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:24:08.414940Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=4.4 json_ms=0.4 total_ms=4.9
2026-05-04T22:24:20.861836Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.2
2026-05-04T22:24:20.881879Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.7
2026-05-04T22:24:21.122125Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.0 json_ms=0.1 total_ms=3.0
2026-05-04T22:24:21.381382Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.2
2026-05-04T22:24:26.240753Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.1 json_ms=0.0 total_ms=1.1
2026-05-04T22:24:26.464684Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.4 json_ms=0.1 total_ms=1.4
2026-05-04T22:24:27.540146Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:24:27.581298Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:24:36.826348Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.3 json_ms=0.0 total_ms=1.4
2026-05-04T22:24:36.827918Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.0
2026-05-04T22:24:39.974077Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:24:39.975731Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:24:44.627129Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=79996 parallel=true cells_before_filter=14 cells_after_filter=13 truncated=false bounds=51.4958,-0.1316,51.5310,-0.0473 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=6.1 json_ms=0.0 total_ms=6.1
2026-05-04T22:24:49.063892Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=79996 filters=5 travel=1 total=37 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=1.9
2026-05-04T22:24:52.794208Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:24:52.795748Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:25:01.609360Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:25:01.611052Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:25:02.161215Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:25:02.170041Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:26:43.296479Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.9 json_ms=0.2 total_ms=1.2
2026-05-04T22:26:45.570271Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=64311 parallel=true cells_before_filter=249 cells_after_filter=231 truncated=false bounds=51.4958,-0.1636,51.5342,-0.0964 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.9 json_ms=0.1 total_ms=1.0
2026-05-04T22:26:46.266290Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:26:46.267461Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:26:46.678793Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=64311 parallel=true cells_before_filter=249 cells_after_filter=231 truncated=false bounds=51.4958,-0.1636,51.5342,-0.0964 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.7 json_ms=0.1 total_ms=0.8
2026-05-04T22:27:31.950199Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:27:31.953653Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:27:32.743995Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:27:32.745372Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:27:36.705103Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:27:36.706272Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:27:40.742291Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:27:42.048355Z INFO property_map_server::routes::features: GET /api/features
2026-05-04T22:27:42.049491Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
2026-05-04T22:27:42.388603Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=152340 parallel=true cells_before_filter=537 cells_after_filter=412 truncated=false bounds=51.4943,-0.1794,51.5357,-0.0806 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=3.7 json_ms=0.4 total_ms=4.1
2026-05-04T22:27:48.759093Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=3047 parallel=false cells_before_filter=155 cells_after_filter=134 truncated=false bounds=51.0079,-1.2718,51.0856,-1.0894 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.1 json_ms=0.1 total_ms=0.2
2026-05-04T22:27:54.979353Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4
2026-05-04T22:27:54.998605Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.0
2026-05-04T22:27:55.235313Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.4 json_ms=0.0 total_ms=2.5
2026-05-04T22:27:55.543332Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=99 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=3.0
2026-05-04T22:28:00.358684Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.0 json_ms=0.1 total_ms=2.1
2026-05-04T22:28:02.432112Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=41 cells_after_filter=32 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.5 json_ms=0.0 total_ms=1.5
2026-05-04T22:28:11.242530Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=122395 parallel=true cells_before_filter=32 cells_after_filter=30 truncated=false bounds=51.4924,-0.1388,51.5338,-0.0399 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.1 json_ms=0.0 total_ms=2.2
2026-05-04T22:28:11.243381Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=122395 filters=5 travel=1 total=76 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=2.9
2026-05-04T22:28:19.048500Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=79996 parallel=true cells_before_filter=14 cells_after_filter=13 truncated=false bounds=51.4958,-0.1316,51.5310,-0.0473 filters=4 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.8 json_ms=0.0 total_ms=1.9
2026-05-04T22:28:19.274325Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=79996 filters=5 travel=1 total=37 filters_raw="Property type:Terraced;;Estimated current price:350000:450000;;Serious crime per 1k residents (avg/yr):0:30;;Noise (dB):50:60" ms=1.5

View file

@ -4,7 +4,7 @@ mod postcodes;
mod property;
pub mod travel_time;
pub use places::PlaceData;
pub use places::{normalize_search_text, PlaceData};
pub use poi::{POICategoryGroup, POIData};
pub use postcodes::{OutcodeData, PostcodeData};
pub use property::{

View file

@ -11,22 +11,127 @@ use crate::utils::InternedColumn;
pub struct PlaceData {
pub name: Vec<String>,
pub name_lower: Vec<String>,
pub name_search: Vec<String>,
pub place_type: InternedColumn,
pub type_rank: Vec<u8>,
pub population: Vec<u32>,
pub lat: Vec<f32>,
pub lon: Vec<f32>,
pub city: Vec<Option<String>>,
pub travel_destination: Vec<bool>,
}
fn type_rank(place_type: &str) -> u8 {
match place_type {
"city" => 0,
"station" => 1,
_ => 2,
"town" => 1,
"village" => 2,
"suburb" | "neighbourhood" | "quarter" | "borough" | "locality" => 3,
"station" => 4,
"hamlet" | "isolated_dwelling" | "island" => 5,
_ => 6,
}
}
pub fn is_travel_destination_type(place_type: &str) -> bool {
matches!(place_type, "city" | "station")
}
pub fn normalize_search_text(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut last_was_space = true;
for ch in text.chars() {
if ch == '\'' || ch == '' || ch == '`' {
continue;
}
let lower = ch.to_ascii_lowercase();
if lower.is_ascii_alphanumeric() {
result.push(lower);
last_was_space = false;
} else if !last_was_space {
result.push(' ');
last_was_space = true;
}
}
if result.ends_with(' ') {
result.pop();
}
result
}
fn replace_token(text: &str, from: &str, to: &str) -> Option<String> {
let mut changed = false;
let replaced: Vec<&str> = text
.split_whitespace()
.map(|token| {
if token == from {
changed = true;
to
} else {
token
}
})
.collect();
changed.then(|| replaced.join(" "))
}
fn push_alias(aliases: &mut Vec<String>, alias: String) {
if !alias.is_empty() && !aliases.iter().any(|existing| existing == &alias) {
aliases.push(alias);
}
}
fn build_search_text(name: &str, place_type: &str) -> String {
let primary = normalize_search_text(name);
let mut aliases = vec![primary.clone()];
if let Some(alias) = replace_token(&primary, "st", "saint") {
push_alias(&mut aliases, alias);
}
if let Some(alias) = replace_token(&primary, "saint", "st") {
push_alias(&mut aliases, alias);
}
if place_type == "station" {
let suffix_aliases: [(&str, &[&str]); 5] = [
(
" tube station",
&[" underground station", " station", " tube", " underground"],
),
(
" underground station",
&[" tube station", " station", " tube", " underground"],
),
(
" railway station",
&[" rail station", " station", " railway", " rail"],
),
(
" overground station",
&[" station", " overground", " railway station"],
),
(
" elizabeth line station",
&[" station", " elizabeth line", " crossrail station"],
),
];
for (suffix, replacements) in suffix_aliases {
if let Some(stem) = primary.strip_suffix(suffix) {
for replacement in replacements {
push_alias(&mut aliases, format!("{stem}{replacement}"));
}
}
}
}
aliases.join(" | ")
}
fn extract_str_col(df: &DataFrame, name: &str) -> anyhow::Result<Vec<String>> {
let column = df
.column(name)
@ -56,6 +161,23 @@ fn extract_f32_col(df: &DataFrame, name: &str) -> anyhow::Result<Vec<f32>> {
.collect())
}
fn extract_bool_col_or_default(
df: &DataFrame,
name: &str,
default_value: bool,
) -> anyhow::Result<Vec<bool>> {
let Ok(column) = df.column(name) else {
return Ok(vec![default_value; df.height()]);
};
let bool_column = column
.bool()
.with_context(|| format!("Column '{name}' is not a boolean column"))?;
Ok(bool_column
.into_iter()
.map(|value| value.unwrap_or(default_value))
.collect())
}
impl PlaceData {
pub fn load(parquet_path: &Path) -> anyhow::Result<Self> {
info!("Loading place data from {:?}...", parquet_path);
@ -80,8 +202,21 @@ impl PlaceData {
};
let name_lower: Vec<String> = name.iter().map(|nm| nm.to_lowercase()).collect();
let name_search: Vec<String> = name
.iter()
.zip(&place_type_raw)
.map(|(nm, pt)| build_search_text(nm, pt))
.collect();
let type_rank_vec: Vec<u8> = place_type_raw.iter().map(|pt| type_rank(pt)).collect();
let place_type = InternedColumn::build(&place_type_raw);
let travel_destination = if df.column("travel_destination").is_ok() {
extract_bool_col_or_default(&df, "travel_destination", true)?
} else {
place_type_raw
.iter()
.map(|place_type| is_travel_destination_type(place_type))
.collect()
};
// Precompute nearest city for each non-city place
let city_indices: Vec<usize> = type_rank_vec
@ -133,12 +268,14 @@ impl PlaceData {
Ok(PlaceData {
name,
name_lower,
name_search,
place_type,
type_rank: type_rank_vec,
population,
lat,
lon,
city,
travel_destination,
})
}
}
@ -149,7 +286,23 @@ mod tests {
#[test]
fn type_rank_ordering() {
assert!(type_rank("city") < type_rank("station"));
assert!(type_rank("city") < type_rank("town"));
assert!(type_rank("town") < type_rank("station"));
assert!(type_rank("station") < type_rank("unknown"));
}
#[test]
fn search_text_handles_common_address_variants() {
assert!(build_search_text("King's Cross tube station", "station")
.contains("kings cross underground"));
assert!(build_search_text("St Albans", "city").contains("saint albans"));
}
#[test]
fn travel_destination_types_match_legacy_places() {
assert!(is_travel_destination_type("city"));
assert!(is_travel_destination_type("station"));
assert!(!is_travel_destination_type("town"));
assert!(!is_travel_destination_type("suburb"));
}
}

View file

@ -5,11 +5,16 @@ use rayon::prelude::*;
use serde::Serialize;
use std::path::Path;
use rustc_hash::FxHashMap;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::consts::{H3_PRECOMPUTE_MAX, HISTOGRAM_BINS, NAN_U16, QUANT_SCALE};
use crate::features::{self, Bounds};
const ADDRESS_SEARCH_CANDIDATE_LIMIT: usize = 50_000;
const ADDRESS_SEARCH_MAX_POSTINGS_PER_TOKEN: usize = 250_000;
const ADDRESS_SEARCH_PREFIX_MIN_LEN: usize = 4;
const ADDRESS_SEARCH_PREFIX_MAX_LEN: usize = 8;
fn is_numeric_dtype(dtype: &DataType) -> bool {
matches!(
dtype,
@ -32,6 +37,360 @@ fn is_datetime_dtype(dtype: &DataType) -> bool {
matches!(dtype, DataType::Datetime(_, _) | DataType::Date)
}
#[derive(Clone, Debug)]
struct AddressTermGroup {
alternatives: Vec<String>,
}
#[derive(Debug)]
struct AddressQuery {
full_postcode: Option<String>,
text_groups: Vec<AddressTermGroup>,
numeric_terms: Vec<String>,
candidate_terms: Vec<String>,
}
fn tokenize_address_text(text: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut current = String::new();
for ch in text.chars() {
if ch.is_ascii_alphanumeric() {
current.push(ch.to_ascii_lowercase());
} else if matches!(ch, '\'' | '' | '`') {
continue;
} else if !current.is_empty() {
tokens.push(std::mem::take(&mut current));
}
}
if !current.is_empty() {
tokens.push(current);
}
tokens
}
fn is_full_postcode_compact(compact: &str) -> bool {
let bytes = compact.as_bytes();
let len = bytes.len();
if !(5..=7).contains(&len) {
return false;
}
let inward = &bytes[len - 3..];
if !inward[0].is_ascii_digit()
|| !inward[1].is_ascii_alphabetic()
|| !inward[2].is_ascii_alphabetic()
{
return false;
}
let outward = &bytes[..len - 3];
if !(2..=4).contains(&outward.len()) {
return false;
}
outward[0].is_ascii_alphabetic()
&& outward.iter().all(u8::is_ascii_alphanumeric)
&& outward.iter().any(u8::is_ascii_digit)
}
fn canonical_postcode_from_compact(compact: &str) -> String {
let upper = compact.to_ascii_uppercase();
let split = upper.len() - 3;
format!("{} {}", &upper[..split], &upper[split..])
}
fn extract_full_postcode(tokens: &[String]) -> Option<(String, Vec<usize>)> {
for (idx, token) in tokens.iter().enumerate() {
let compact = token.to_ascii_uppercase();
if is_full_postcode_compact(&compact) {
return Some((canonical_postcode_from_compact(&compact), vec![idx]));
}
}
for idx in 0..tokens.len().saturating_sub(1) {
let compact = format!(
"{}{}",
tokens[idx].to_ascii_uppercase(),
tokens[idx + 1].to_ascii_uppercase()
);
if is_full_postcode_compact(&compact) {
return Some((
canonical_postcode_from_compact(&compact),
vec![idx, idx + 1],
));
}
}
None
}
fn looks_like_postcode_fragment(token: &str) -> bool {
(2..=4).contains(&token.len())
&& token
.chars()
.next()
.is_some_and(|ch| ch.is_ascii_alphabetic())
&& token.chars().any(|ch| ch.is_ascii_digit())
&& token.chars().all(|ch| ch.is_ascii_alphanumeric())
}
fn is_numeric_address_token(token: &str) -> bool {
token.chars().all(|ch| ch.is_ascii_digit())
}
fn address_token_aliases(token: &str) -> Vec<&'static str> {
match token {
"apt" => vec!["apt", "apartment"],
"apartment" => vec!["apartment", "apt"],
"ave" => vec!["ave", "avenue"],
"avenue" => vec!["avenue", "ave"],
"blvd" => vec!["blvd", "boulevard"],
"boulevard" => vec!["boulevard", "blvd"],
"cl" => vec!["cl", "close"],
"close" => vec!["close", "cl"],
"ct" => vec!["ct", "court"],
"court" => vec!["court", "ct"],
"cres" => vec!["cres", "crescent"],
"crescent" => vec!["crescent", "cres"],
"dr" => vec!["dr", "drive"],
"drive" => vec!["drive", "dr"],
"fl" => vec!["fl", "flat"],
"flat" => vec!["flat", "fl"],
"gdns" => vec!["gdns", "gardens", "garden"],
"garden" => vec!["garden", "gardens", "gdns"],
"gardens" => vec!["gardens", "garden", "gdns"],
"hse" => vec!["hse", "house"],
"house" => vec!["house", "hse"],
"ln" => vec!["ln", "lane"],
"lane" => vec!["lane", "ln"],
"rd" => vec!["rd", "road"],
"road" => vec!["road", "rd"],
"sq" => vec!["sq", "square"],
"square" => vec!["square", "sq"],
"st" => vec!["st", "street", "saint"],
"street" => vec!["street", "st"],
"saint" => vec!["saint", "st"],
"terr" => vec!["terr", "terrace"],
"terrace" => vec!["terrace", "terr"],
_ => Vec::new(),
}
}
fn is_address_stop_token(token: &str) -> bool {
matches!(
token,
"a" | "an"
| "and"
| "apartment"
| "apt"
| "avenue"
| "ave"
| "block"
| "building"
| "bungalow"
| "close"
| "cl"
| "court"
| "ct"
| "cres"
| "crescent"
| "drive"
| "dr"
| "estate"
| "flat"
| "fl"
| "floor"
| "garden"
| "gardens"
| "gdns"
| "grove"
| "house"
| "hse"
| "lane"
| "ln"
| "lodge"
| "mansions"
| "mews"
| "of"
| "park"
| "place"
| "road"
| "rd"
| "room"
| "row"
| "saint"
| "sq"
| "square"
| "st"
| "street"
| "terr"
| "terrace"
| "the"
| "unit"
| "view"
| "villas"
| "walk"
| "way"
| "yard"
)
}
fn address_term_group(token: &str) -> Option<AddressTermGroup> {
if token.len() < 3 || is_numeric_address_token(token) || looks_like_postcode_fragment(token) {
return None;
}
let mut alternatives = Vec::new();
alternatives.push(token.to_string());
for alias in address_token_aliases(token) {
if !alternatives.iter().any(|existing| existing == alias) {
alternatives.push(alias.to_string());
}
}
if alternatives
.iter()
.all(|alternative| is_address_stop_token(alternative))
{
return None;
}
Some(AddressTermGroup { alternatives })
}
fn address_search_tokens(text: &str) -> Vec<String> {
let mut tokens: Vec<String> = tokenize_address_text(text)
.into_iter()
.filter(|token| is_address_search_token(token))
.collect();
tokens.sort_unstable();
tokens.dedup();
tokens
}
fn is_address_search_token(token: &str) -> bool {
if looks_like_postcode_fragment(token) {
return false;
}
if is_numeric_address_token(token) {
return true;
}
if token.chars().any(|ch| ch.is_ascii_digit()) {
return token.len() >= 2;
}
token.len() >= 3
}
fn is_address_candidate_token(token: &str) -> bool {
!is_numeric_address_token(token)
&& !looks_like_postcode_fragment(token)
&& (token.chars().any(|ch| ch.is_ascii_digit())
|| (token.len() >= 3 && !is_address_stop_token(token)))
}
fn address_prefix_key(term: &str) -> &str {
if term.len() > ADDRESS_SEARCH_PREFIX_MAX_LEN {
&term[..ADDRESS_SEARCH_PREFIX_MAX_LEN]
} else {
term
}
}
fn build_address_prefix_index(
address_token_index: &FxHashMap<String, Vec<u32>>,
) -> FxHashMap<String, Vec<String>> {
let mut prefix_index: FxHashMap<String, Vec<String>> = FxHashMap::default();
for token in address_token_index.keys() {
let max_prefix_len = token.len().min(ADDRESS_SEARCH_PREFIX_MAX_LEN);
for prefix_len in ADDRESS_SEARCH_PREFIX_MIN_LEN..=max_prefix_len {
prefix_index
.entry(token[..prefix_len].to_string())
.or_default()
.push(token.clone());
}
}
for tokens in prefix_index.values_mut() {
tokens.sort_unstable();
tokens.dedup();
}
prefix_index
}
fn parse_address_query(query: &str) -> AddressQuery {
let tokens = tokenize_address_text(query);
let (full_postcode, postcode_token_indices) = extract_full_postcode(&tokens)
.map(|(postcode, indices)| (Some(postcode), indices))
.unwrap_or((None, Vec::new()));
let skip_postcode_tokens: FxHashSet<usize> = postcode_token_indices.into_iter().collect();
let mut text_groups = Vec::new();
let mut numeric_terms = Vec::new();
let mut candidate_terms = Vec::new();
for (idx, token) in tokens.iter().enumerate() {
if skip_postcode_tokens.contains(&idx) || looks_like_postcode_fragment(token) {
continue;
}
if is_numeric_address_token(token) {
numeric_terms.push(token.clone());
continue;
}
if let Some(group) = address_term_group(token) {
for alternative in &group.alternatives {
if !is_address_stop_token(alternative)
&& !candidate_terms.iter().any(|term| term == alternative)
{
candidate_terms.push(alternative.clone());
}
}
text_groups.push(group);
} else if token.chars().any(|ch| ch.is_ascii_digit()) && token.len() >= 2 {
numeric_terms.push(token.clone());
if !candidate_terms.iter().any(|term| term == token) {
candidate_terms.push(token.clone());
}
}
}
text_groups.dedup_by(|left, right| left.alternatives == right.alternatives);
numeric_terms.sort_unstable();
numeric_terms.dedup();
AddressQuery {
full_postcode,
text_groups,
numeric_terms,
candidate_terms,
}
}
fn token_matches_query_term(token: &str, query_term: &str) -> bool {
token == query_term || (query_term.len() >= 3 && token.starts_with(query_term))
}
fn token_matches_numeric_term(token: &str, query_term: &str) -> bool {
token == query_term || token.starts_with(query_term)
}
fn address_tokens_match_group(tokens: &[String], group: &AddressTermGroup) -> bool {
group.alternatives.iter().any(|alternative| {
tokens
.iter()
.any(|token| token_matches_query_term(token, alternative))
})
}
/// Histogram with outlier buckets at the edges.
/// - Bin 0: [min, p1) — low outliers
/// - Bins 1 to n-2: [p1, p99) — main distribution, evenly divided
@ -163,6 +522,20 @@ pub struct PropertyData {
/// Interned postcodes: reader is thread-safe, keys index into it.
postcode_interner: lasso::RodeoReader,
postcode_keys: Vec<lasso::Spur>,
/// Rows for each postcode, keyed by the interned postcode key.
postcode_row_index: FxHashMap<lasso::Spur, Vec<u32>>,
/// Inverted index from address tokens to property rows.
address_token_index: FxHashMap<String, Vec<u32>>,
/// Prefix lookup from typed address-token prefix to indexed full address tokens.
address_prefix_index: FxHashMap<String, Vec<String>>,
/// Interned normalized address-search tokens used for per-row scoring.
address_search_interner: lasso::RodeoReader,
/// Flat per-row normalized address-search token keys.
address_search_token_keys: Vec<lasso::Spur>,
/// Offset into `address_search_token_keys` for each row.
address_search_token_offsets: Vec<u32>,
/// Number of normalized address-search token keys for each row.
address_search_token_lengths: Vec<u16>,
/// For enum features: maps feature index to list of possible string values.
/// Index in values list corresponds to the u16 value stored in feature_data.
pub enum_values: rustc_hash::FxHashMap<usize, Vec<String>>,
@ -197,6 +570,164 @@ impl PropertyData {
(&self.postcode_interner, &self.postcode_keys)
}
fn row_address_search_tokens(&self, row: usize) -> &[lasso::Spur] {
let offset = self.address_search_token_offsets[row] as usize;
let length = self.address_search_token_lengths[row] as usize;
&self.address_search_token_keys[offset..offset + length]
}
/// Search individual property addresses. Full postcode queries use a direct row index;
/// free-text queries use a small inverted index over distinctive address tokens.
pub fn search_addresses(&self, query: &str, limit: usize) -> Vec<usize> {
if limit == 0 {
return Vec::new();
}
let parsed = parse_address_query(query);
if parsed.full_postcode.is_none()
&& parsed.text_groups.is_empty()
&& parsed.numeric_terms.is_empty()
{
return Vec::new();
}
let candidate_rows: Vec<u32> = if let Some(postcode) = parsed.full_postcode.as_deref() {
self.postcode_interner
.get(postcode)
.and_then(|key| self.postcode_row_index.get(&key))
.map(|rows| rows.to_vec())
.unwrap_or_default()
} else if let Some(rows) = self.best_address_token_rows(&parsed.candidate_terms) {
rows.iter()
.take(ADDRESS_SEARCH_CANDIDATE_LIMIT)
.copied()
.collect()
} else {
Vec::new()
};
if candidate_rows.is_empty() {
return Vec::new();
}
let mut scored: Vec<(i32, usize, usize)> = candidate_rows
.into_iter()
.filter_map(|row| {
let row = row as usize;
self.address_match_score(row, &parsed)
.map(|score| (score, self.address(row).len(), row))
})
.collect();
scored.sort_unstable_by(|left, right| {
right
.0
.cmp(&left.0)
.then(left.1.cmp(&right.1))
.then(left.2.cmp(&right.2))
});
let mut seen = FxHashSet::default();
let mut results = Vec::with_capacity(limit);
for (_, _, row) in scored {
let address = self.address(row).trim();
if address.is_empty() {
continue;
}
let key = format!("{}\n{}", address.to_ascii_lowercase(), self.postcode(row));
if !seen.insert(key) {
continue;
}
results.push(row);
if results.len() == limit {
break;
}
}
results
}
fn best_address_token_rows(&self, terms: &[String]) -> Option<&[u32]> {
let mut best: Option<&[u32]> = None;
for term in terms {
if let Some(rows) = self.address_token_index.get(term) {
if best.map_or(true, |current| rows.len() < current.len()) {
best = Some(rows.as_slice());
}
continue;
}
if term.len() < 4 {
continue;
}
if let Some(tokens) = self.address_prefix_index.get(address_prefix_key(term)) {
for token in tokens {
if !token.starts_with(term) {
continue;
}
if let Some(rows) = self.address_token_index.get(token) {
if best.map_or(true, |current| rows.len() < current.len()) {
best = Some(rows.as_slice());
}
}
}
}
}
best
}
fn address_match_score(&self, row: usize, parsed: &AddressQuery) -> Option<i32> {
if self.address(row).trim().is_empty() {
return None;
}
let tokens = self.row_address_search_tokens(row);
if parsed
.text_groups
.iter()
.any(|group| !self.address_tokens_match_group(tokens, group))
{
return None;
}
let numeric_matches = parsed
.numeric_terms
.iter()
.filter(|term| {
tokens.iter().any(|token| {
token_matches_numeric_term(self.address_search_interner.resolve(token), term)
})
})
.count();
if !parsed.numeric_terms.is_empty() && numeric_matches == 0 {
return None;
}
let mut score = 0;
if parsed.full_postcode.is_some() {
score += 1_000;
}
score += (parsed.text_groups.len() as i32) * 200;
score += (numeric_matches as i32) * 90;
if numeric_matches == parsed.numeric_terms.len() && numeric_matches > 0 {
score += 50;
}
Some(score)
}
fn address_tokens_match_group(&self, tokens: &[lasso::Spur], group: &AddressTermGroup) -> bool {
group.alternatives.iter().any(|alternative| {
tokens.iter().any(|token| {
token_matches_query_term(self.address_search_interner.resolve(token), alternative)
})
})
}
/// Get the is_approx_build_date flag for a given row (bit-packed).
pub fn is_approx_build_date(&self, row: usize) -> bool {
let byte = self.approx_build_date_bits[row / 8];
@ -946,27 +1477,70 @@ impl PropertyData {
.map(|&perm_index| lon[perm_index as usize])
.collect();
// Build contiguous address buffer (permuted)
// Build contiguous address buffer and address search index (permuted)
tracing::info!("Building interned strings");
let total_addr_bytes: usize = address_raw.iter().map(|text| text.len()).sum();
let mut address_buffer = String::with_capacity(total_addr_bytes);
let mut address_offsets = Vec::with_capacity(row_count);
let mut address_lengths = Vec::with_capacity(row_count);
for &perm_index in &perm {
let mut address_token_index: FxHashMap<String, Vec<u32>> = FxHashMap::default();
let mut address_search_rodeo = lasso::Rodeo::default();
let mut address_search_token_keys: Vec<lasso::Spur> = Vec::new();
let mut address_search_token_offsets = Vec::with_capacity(row_count);
let mut address_search_token_lengths = Vec::with_capacity(row_count);
for (new_row, &perm_index) in perm.iter().enumerate() {
let addr = &address_raw[perm_index as usize];
let offset = address_buffer.len() as u32;
let length = addr.len().min(u16::MAX as usize) as u16;
address_offsets.push(offset);
address_lengths.push(length);
address_buffer.push_str(&addr[..length as usize]);
let search_tokens = address_search_tokens(addr);
let token_offset = address_search_token_keys.len() as u32;
let token_length = search_tokens.len().min(u16::MAX as usize) as u16;
address_search_token_offsets.push(token_offset);
address_search_token_lengths.push(token_length);
for token in search_tokens.iter().take(token_length as usize) {
let key = address_search_rodeo.get_or_intern(token);
address_search_token_keys.push(key);
if is_address_candidate_token(token) {
address_token_index
.entry(token.clone())
.or_default()
.push(new_row as u32);
}
}
}
let address_token_count_before_prune = address_token_index.len();
address_token_index.retain(|_, rows| rows.len() <= ADDRESS_SEARCH_MAX_POSTINGS_PER_TOKEN);
let address_prefix_index = build_address_prefix_index(&address_token_index);
let address_search_interner = address_search_rodeo.into_reader();
let address_postings_count: usize = address_token_index.values().map(Vec::len).sum();
tracing::info!(
tokens = address_token_index.len(),
prefixes = address_prefix_index.len(),
pruned_tokens =
address_token_count_before_prune.saturating_sub(address_token_index.len()),
postings = address_postings_count,
row_tokens = address_search_token_keys.len(),
"Address search index built"
);
// Intern postcodes (permuted)
let mut postcode_rodeo = lasso::Rodeo::default();
let postcode_keys: Vec<lasso::Spur> = perm
.iter()
.map(|&perm_index| postcode_rodeo.get_or_intern(&postcode_raw[perm_index as usize]))
.collect();
let mut postcode_keys: Vec<lasso::Spur> = Vec::with_capacity(row_count);
let mut postcode_row_index: FxHashMap<lasso::Spur, Vec<u32>> = FxHashMap::default();
for (new_row, &perm_index) in perm.iter().enumerate() {
let key = postcode_rodeo.get_or_intern(&postcode_raw[perm_index as usize]);
postcode_keys.push(key);
postcode_row_index
.entry(key)
.or_default()
.push(new_row as u32);
}
let postcode_interner = postcode_rodeo.into_reader();
// Pack is_approx_build_date into a bitvec (8 bools per byte)
@ -1110,6 +1684,13 @@ impl PropertyData {
address_lengths,
postcode_interner,
postcode_keys,
postcode_row_index,
address_token_index,
address_prefix_index,
address_search_interner,
address_search_token_keys,
address_search_token_offsets,
address_search_token_lengths,
enum_values,
enum_counts,
approx_build_date_bits,
@ -1133,6 +1714,120 @@ mod tests {
Bounds::Percentile { low, high }
}
#[test]
fn full_postcode_detection_accepts_common_formats() {
assert!(is_full_postcode_compact("SW1A1AA"));
assert!(is_full_postcode_compact("E142DG"));
assert!(is_full_postcode_compact("M11AE"));
assert!(!is_full_postcode_compact("E14"));
assert!(!is_full_postcode_compact("DOWNING"));
assert!(!is_full_postcode_compact("10A"));
}
#[test]
fn address_query_parsing_skips_postcodes_and_street_suffixes() {
let parsed = parse_address_query("Flat 2, 10 Downing St, SW1A 2AA");
assert_eq!(parsed.full_postcode.as_deref(), Some("SW1A 2AA"));
assert_eq!(
parsed.numeric_terms,
vec!["10".to_string(), "2".to_string()]
);
assert_eq!(parsed.candidate_terms, vec!["downing".to_string()]);
assert_eq!(parsed.text_groups.len(), 1);
assert_eq!(
parsed.text_groups[0].alternatives,
vec!["downing".to_string()]
);
}
#[test]
fn address_query_parsing_handles_compact_postcodes() {
let parsed = parse_address_query("10 downing street sw1a1aa");
assert_eq!(parsed.full_postcode.as_deref(), Some("SW1A 1AA"));
assert_eq!(parsed.numeric_terms, vec!["10".to_string()]);
assert_eq!(parsed.candidate_terms, vec!["downing".to_string()]);
}
#[test]
fn address_query_parsing_keeps_partial_terms_for_row_matching() {
let parsed = parse_address_query("settlers cour");
assert_eq!(parsed.full_postcode, None);
assert_eq!(parsed.numeric_terms, Vec::<String>::new());
assert_eq!(
parsed.candidate_terms,
vec!["settlers".to_string(), "cour".to_string()]
);
assert_eq!(parsed.text_groups.len(), 2);
assert_eq!(
parsed.text_groups[0].alternatives,
vec!["settlers".to_string()]
);
assert_eq!(parsed.text_groups[1].alternatives, vec!["cour".to_string()]);
}
#[test]
fn address_search_tokens_keep_actual_address_terms_for_scoring() {
let tokens = address_search_tokens("Flat 2, 10 Downing Cour");
assert_eq!(
tokens,
vec![
"10".to_string(),
"2".to_string(),
"cour".to_string(),
"downing".to_string(),
"flat".to_string()
]
);
}
#[test]
fn address_prefix_index_finds_partial_address_terms() {
let mut token_index: FxHashMap<String, Vec<u32>> = FxHashMap::default();
token_index.insert("downing".to_string(), vec![1]);
token_index.insert("downton".to_string(), vec![2]);
token_index.insert("market".to_string(), vec![3]);
let prefix_index = build_address_prefix_index(&token_index);
assert_eq!(
prefix_index.get("down").cloned().unwrap_or_default(),
vec!["downing".to_string(), "downton".to_string()]
);
assert_eq!(
prefix_index.get("downi").cloned().unwrap_or_default(),
vec!["downing".to_string()]
);
assert_eq!(
prefix_index.get("downt").cloned().unwrap_or_default(),
vec!["downton".to_string()]
);
assert!(!prefix_index.contains_key("do"));
}
#[test]
fn address_term_matching_allows_prefixes_and_aliases() {
let tokens = tokenize_address_text("10 Downing Street");
let prefix_group = address_term_group("down").expect("prefix term should be searchable");
let alias_group = AddressTermGroup {
alternatives: vec!["st".to_string(), "street".to_string()],
};
assert!(address_tokens_match_group(&tokens, &prefix_group));
assert!(address_tokens_match_group(&tokens, &alias_group));
}
#[test]
fn address_term_matching_uses_actual_token_prefixes() {
let tokens = tokenize_address_text("12 Settlers Court");
let prefix_group = address_term_group("cou").expect("partial term should be searchable");
assert!(address_tokens_match_group(&tokens, &prefix_group));
}
#[test]
fn histogram_empty_data() {
let data: Vec<f32> = vec![];

View file

@ -413,7 +413,7 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
features: &[
Feature::Numeric(FeatureConfig {
name: "Income Score (rate)",
bounds: Bounds::Fixed { min: 0.0, max: 0.6 },
bounds: Bounds::Fixed { min: 0.0, max: 1.0 },
step: 0.01,
description: "Income deprivation rate, inverted (higher = less deprived)",
detail: "From the English Indices of Deprivation (inverted so higher = better). Higher values indicate less income deprivation. Based on Income Support, income-based Jobseeker's Allowance, income-based Employment and Support Allowance, Pension Credit, Working Tax Credit and Child Tax Credit, Universal Credit, and asylum seekers.",
@ -425,7 +425,7 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
}),
Feature::Numeric(FeatureConfig {
name: "Employment Score (rate)",
bounds: Bounds::Fixed { min: 0.0, max: 0.4 },
bounds: Bounds::Fixed { min: 0.0, max: 1.0 },
step: 0.01,
description: "Employment deprivation rate, inverted (higher = less deprived)",
detail: "From the English Indices of Deprivation (inverted so higher = better). Higher values indicate less employment deprivation. Based on claimants of Jobseeker's Allowance, Employment and Support Allowance, Incapacity Benefit, Severe Disablement Allowance, Carer's Allowance, and relevant Universal Credit claimants.",

View file

@ -1,16 +1,26 @@
use std::sync::Arc;
use axum::body::Body;
use axum::body::{to_bytes, Body};
use axum::extract::Request;
use axum::http::header;
use axum::http::{header, StatusCode};
use axum::middleware::Next;
use axum::response::Response;
use tracing::warn;
use crate::state::AppState;
const OG_PLACEHOLDER: &str =
r#"<meta name="x-og-placeholder" content="__PERFECT_POSTCODE_OG_TAGS__"/>"#;
const HTML_BODY_LIMIT: usize = 5 * 1024 * 1024;
struct SeoPage {
canonical_path: &'static str,
title: &'static str,
description: &'static str,
indexable: bool,
}
/// Escape a string for safe inclusion inside a double-quoted HTML attribute value.
fn escape_attr(s: &str) -> String {
let mut out = String::with_capacity(s.len());
@ -26,6 +36,279 @@ fn escape_attr(s: &str) -> String {
out
}
fn trim_trailing_slash(path: &str) -> &str {
if path.len() > 1 {
path.trim_end_matches('/')
} else {
path
}
}
fn seo_page_for_path(path: &str) -> Option<SeoPage> {
let path = trim_trailing_slash(path);
match path {
"/" => Some(SeoPage {
canonical_path: "/",
title: "Perfect Postcode - Find where to buy before browsing listings",
description: "Search every postcode by budget, commute, schools, safety, noise, broadband, prices and more. Build a better home-buying shortlist before viewings.",
indexable: true,
}),
"/learn" | "/support" => Some(SeoPage {
canonical_path: "/learn",
title: "How Perfect Postcode works - Data sources, FAQ and support",
description: "Learn how Perfect Postcode combines property prices, EPC records, travel times, crime, schools, broadband, noise, amenities and open data for postcode research.",
indexable: true,
}),
"/pricing" => Some(SeoPage {
canonical_path: "/pricing",
title: "Perfect Postcode pricing - Lifetime property search map access",
description: "Get lifetime access to the postcode property search map for England, including filters, saved searches, exports, and future data updates.",
indexable: true,
}),
"/property-price-map" => Some(SeoPage {
canonical_path: "/property-price-map",
title: "Property price map for England - Compare postcodes before viewing",
description: "Compare sold prices, estimated current value, price per square metre and local context across English postcodes before searching listings.",
indexable: true,
}),
"/postcode-property-search" => Some(SeoPage {
canonical_path: "/postcode-property-search",
title: "Postcode property search - Find areas that match your criteria",
description: "Search every postcode by budget, property type, floor area, tenure, commute, schools, crime, broadband, noise, parks and local amenities.",
indexable: true,
}),
"/commute-property-search" => Some(SeoPage {
canonical_path: "/commute-property-search",
title: "Commute property search - Find places to live by travel time",
description: "Filter postcodes by commute time, then compare price, schools, safety, broadband, road noise, parks and property data on one map.",
indexable: true,
}),
"/school-property-search" => Some(SeoPage {
canonical_path: "/school-property-search",
title: "School property search - Compare postcodes for family moves",
description: "Compare nearby schools, property size, prices, parks, safety, commute and local amenities before building a viewing shortlist.",
indexable: true,
}),
"/postcode-checker" => Some(SeoPage {
canonical_path: "/postcode-checker",
title: "Postcode checker - Property, crime, broadband, noise and schools",
description: "Check postcode-level property prices, EPC data, crime, broadband, road noise, schools, council tax, amenities and travel-time context.",
indexable: true,
}),
"/property-search/birmingham" => Some(SeoPage {
canonical_path: "/property-search/birmingham",
title: "Birmingham property search - Compare postcodes by price and commute",
description: "Use postcode-level data to compare Birmingham property prices, commute trade-offs, schools, crime, broadband and local amenities before viewings.",
indexable: true,
}),
"/property-search/manchester" => Some(SeoPage {
canonical_path: "/property-search/manchester",
title: "Manchester property search - Compare postcodes before viewing",
description: "Compare Manchester-area postcodes by budget, commute, property type, schools, broadband, crime, noise and amenities before booking viewings.",
indexable: true,
}),
"/property-search/bristol" => Some(SeoPage {
canonical_path: "/property-search/bristol",
title: "Bristol property search - Compare postcodes by commute and price",
description: "Compare Bristol postcodes by price, commute, property size, schools, broadband, crime, road noise, parks and amenities before viewings.",
indexable: true,
}),
"/data-sources" => Some(SeoPage {
canonical_path: "/data-sources",
title: "Perfect Postcode data sources - Property, schools, commute and local context",
description: "Review the public and official datasets used by Perfect Postcode, including property prices, EPC, schools, crime, broadband, noise and travel-time context.",
indexable: true,
}),
"/methodology" => Some(SeoPage {
canonical_path: "/methodology",
title: "Perfect Postcode methodology - How to interpret postcode property data",
description: "Understand how to use postcode filters, property estimates, travel-time data, school context and local signals as a home-buying shortlist tool.",
indexable: true,
}),
"/privacy-security" => Some(SeoPage {
canonical_path: "/privacy-security",
title: "Perfect Postcode privacy and security - Saved searches and account data",
description: "Learn how Perfect Postcode treats saved searches, account data and property research workflows with privacy and security in mind.",
indexable: true,
}),
"/dashboard" => Some(SeoPage {
canonical_path: "/dashboard",
title: "Perfect Postcode dashboard",
description: "Explore postcode property data, travel times, prices, schools, crime, noise, broadband and amenities on the interactive map.",
indexable: false,
}),
"/saved" => Some(SeoPage {
canonical_path: "/saved",
title: "Perfect Postcode account",
description: "Manage your Perfect Postcode account, saved searches, saved properties and invitations.",
indexable: false,
}),
"/invites" => Some(SeoPage {
canonical_path: "/invites",
title: "Perfect Postcode account",
description: "Manage your Perfect Postcode account, saved searches, saved properties and invitations.",
indexable: false,
}),
"/account" => Some(SeoPage {
canonical_path: "/account",
title: "Perfect Postcode account",
description: "Manage your Perfect Postcode account, saved searches, saved properties and invitations.",
indexable: false,
}),
_ if path.starts_with("/invite/") => Some(SeoPage {
canonical_path: "/invite",
title: "You're invited to Perfect Postcode",
description: "Accept your invitation to explore property prices, energy ratings, crime stats, school ratings, and more across England.",
indexable: false,
}),
_ => None,
}
}
fn is_passthrough_path(path: &str) -> bool {
path.starts_with("/api/")
|| path.starts_with("/pb/")
|| path.starts_with("/s/")
|| path.starts_with("/assets/")
|| matches!(
path,
"/health"
| "/metrics"
| "/robots.txt"
| "/sitemap.xml"
| "/favicon.svg"
| "/bundle.js"
| "/main.css"
| "/house.png"
)
|| path
.rsplit('/')
.next()
.is_some_and(|segment| segment.contains('.'))
}
fn should_return_404(path: &str) -> bool {
!is_passthrough_path(path) && seo_page_for_path(path).is_none()
}
fn not_found_response(public_url: &str, path: &str) -> Response {
let public_url_e = escape_attr(public_url);
let path_e = escape_attr(path);
let html = format!(
r#"<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex,follow" />
<title>Page not found - Perfect Postcode</title>
<meta name="description" content="This Perfect Postcode page could not be found." />
<link rel="canonical" href="{public_url_e}/" />
</head>
<body>
<main>
<h1>Page not found</h1>
<p>The requested path was not found: {path_e}</p>
<p><a href="{public_url_e}/">Go to Perfect Postcode</a></p>
</main>
</body>
</html>"#
);
let mut response = Response::new(Body::from(html));
*response.status_mut() = StatusCode::NOT_FOUND;
response.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/html; charset=utf-8"),
);
response
}
fn route_seo_tags(page: &SeoPage, path: &str, query_string: &str, public_url: &str) -> String {
let path_e = escape_attr(path);
let query_e = escape_attr(query_string);
let public_url_e = escape_attr(public_url.trim_end_matches('/'));
let canonical_path_e = escape_attr(page.canonical_path);
let title_e = escape_attr(page.title);
let description_e = escape_attr(page.description);
let is_invite = path.starts_with("/invite/");
let og_image_url = if is_invite {
if query_string.is_empty() {
format!("{public_url_e}/api/screenshot?og=1&amp;path={path_e}")
} else {
format!("{public_url_e}/api/screenshot?og=1&amp;path={path_e}&amp;{query_e}")
}
} else if query_string.is_empty() {
format!("{public_url_e}/api/screenshot?og=1")
} else {
format!("{public_url_e}/api/screenshot?og=1&amp;{query_e}")
};
let canonical_url = format!("{public_url_e}{canonical_path_e}");
let og_url = if query_string.is_empty() {
format!("{public_url_e}{path_e}")
} else {
format!("{public_url_e}{path_e}?{query_e}")
};
let robots = if page.indexable {
"index,follow"
} else {
"noindex,follow"
};
format!(
r#"<meta name="robots" content="{robots}" />
<link rel="canonical" href="{canonical_url}" />
<meta property="og:title" content="{title_e}" />
<meta property="og:description" content="{description_e}" />
<meta property="og:type" content="website" />
<meta property="og:url" content="{og_url}" />
<meta property="og:site_name" content="Perfect Postcode" />
<meta property="og:logo" content="{public_url_e}/favicon.svg" />
<meta property="og:image" content="{og_image_url}" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{title_e}" />
<meta name="twitter:description" content="{description_e}" />
<meta name="twitter:image" content="{og_image_url}" />"#
)
}
fn inject_tags(mut html: String, page: &SeoPage, tags: &str) -> String {
if let Some(start) = html.find("<title>") {
if let Some(end_offset) = html[start..].find("</title>") {
let end = start + end_offset + "</title>".len();
html.replace_range(
start..end,
&format!("<title>{}</title>", escape_attr(page.title)),
);
}
}
if let Some(start) = html.find(r#"<meta name="description""#) {
if let Some(end_offset) = html[start..].find('>') {
let end = start + end_offset + 1;
html.replace_range(
start..end,
&format!(
r#"<meta name="description" content="{}" />"#,
escape_attr(page.description)
),
);
}
}
if html.contains(OG_PLACEHOLDER) {
return html.replace(OG_PLACEHOLDER, tags);
}
if let Some(index) = html.find("</head>") {
html.insert_str(index, tags);
}
html
}
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
@ -34,6 +317,12 @@ pub async fn og_middleware(request: Request, next: Next) -> Response {
// Get state from extensions
let state = request.extensions().get::<Arc<AppState>>().cloned();
if let Some(st) = &state {
if !st.is_dev && should_return_404(&path) {
return not_found_response(&st.public_url, &path);
}
}
let response = next.run(request).await;
// Only inject OG tags into SPA HTML responses, not proxied PocketBase responses
@ -56,68 +345,25 @@ pub async fn og_middleware(request: Request, next: Next) -> Response {
None => return response,
};
let index_html = match &state.index_html {
Some(html) => html,
let page = match seo_page_for_path(&path) {
Some(page) => page,
None => return response,
};
// Build OG-injected HTML (og=1 triggers heading overlay on screenshot).
// All URL components are HTML-escaped before interpolation into attributes
// because path/query are attacker-controlled.
let is_invite = path.starts_with("/invite/");
let path_e = escape_attr(&path);
let query_e = escape_attr(&query_string);
let public_url_e = escape_attr(&state.public_url);
let og_image_url = if is_invite {
// Include path= so the screenshot service navigates to /invite/CODE
if query_string.is_empty() {
format!("{public_url_e}/api/screenshot?og=1&amp;path={path_e}")
} else {
format!("{public_url_e}/api/screenshot?og=1&amp;path={path_e}&amp;{query_e}")
let (mut parts, body) = response.into_parts();
let bytes = match to_bytes(body, HTML_BODY_LIMIT).await {
Ok(bytes) => bytes,
Err(err) => {
warn!("Failed to buffer HTML body for SEO tag injection: {err}");
let mut response = Response::from_parts(parts, Body::empty());
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
return response;
}
} else if query_string.is_empty() {
format!("{public_url_e}/api/screenshot?og=1")
} else {
format!("{public_url_e}/api/screenshot?og=1&amp;{query_e}")
};
let og_url = if query_string.is_empty() {
format!("{public_url_e}{path_e}")
} else {
format!("{public_url_e}{path_e}?{query_e}")
};
let og_logo = format!("{public_url_e}/favicon.svg");
let (og_title, og_description) = if is_invite {
(
"You\u{2019}re invited to Perfect Postcode",
"Accept your invitation to explore property prices, energy ratings, crime stats, school ratings, and more across England.",
)
} else {
(
"Perfect Postcode \u{2014} Every neighbourhood in England",
"Explore property prices, energy ratings, crime stats, school ratings, and more across England on one interactive map.",
)
};
let og_tags = format!(
r#"<meta property="og:title" content="{og_title}" />
<meta property="og:description" content="{og_description}" />
<meta property="og:type" content="website" />
<meta property="og:url" content="{og_url}" />
<meta property="og:logo" content="{og_logo}" />
<meta property="og:image" content="{og_image_url}" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{og_title}" />
<meta name="twitter:description" content="{og_description}" />"#
);
let html = index_html.replace(OG_PLACEHOLDER, &og_tags);
let (parts, _body) = response.into_parts();
let html = String::from_utf8_lossy(&bytes).into_owned();
let tags = route_seo_tags(&page, &path, &query_string, &state.public_url);
let html = inject_tags(html, &page, &tags);
parts.headers.remove(header::CONTENT_LENGTH);
Response::from_parts(parts, Body::from(html))
}

View file

@ -143,6 +143,9 @@ fn execute_destination_search(state: &AppState, query: &str, mode: &str) -> Valu
.iter()
.enumerate()
.filter_map(|(idx, name_lower)| {
if !pd.travel_destination[idx] {
return None;
}
let words_match = query_words.iter().all(|word| name_lower.contains(word));
let slug = slugify(&pd.name[idx]);
let slug_match = slug.contains(&query_slug) || query_slug.contains(&slug);
@ -169,6 +172,9 @@ fn execute_destination_search(state: &AppState, query: &str, mode: &str) -> Valu
.iter()
.enumerate()
.find_map(|(idx, name_lower)| {
if !pd.travel_destination[idx] {
return None;
}
let words_match = query_words.iter().all(|word| name_lower.contains(word));
let slug = slugify(&pd.name[idx]);
let slug_match = slug.contains(&query_slug) || query_slug.contains(&slug);
@ -186,6 +192,9 @@ fn execute_destination_search(state: &AppState, query: &str, mode: &str) -> Valu
.iter()
.enumerate()
.filter_map(|(idx, city_opt)| {
if !pd.travel_destination[idx] {
return None;
}
let city = city_opt.as_deref()?;
if city.to_lowercase() != city_lower {
return None;

View file

@ -6,7 +6,7 @@ use axum::response::Json;
use serde::{Deserialize, Serialize};
use tracing::info;
use crate::data::slugify;
use crate::data::{normalize_search_text, slugify};
use crate::state::SharedState;
#[derive(Serialize)]
@ -20,9 +20,21 @@ pub struct PlaceResult {
city: Option<String>,
}
#[derive(Serialize)]
pub struct AddressResult {
address: String,
postcode: String,
lat: f32,
lon: f32,
}
#[derive(Serialize)]
pub struct PlacesResponse {
places: Vec<PlaceResult>,
#[serde(skip_serializing_if = "Vec::is_empty")]
postcodes: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
addresses: Vec<AddressResult>,
}
#[derive(Deserialize)]
@ -34,6 +46,53 @@ pub struct PlacesParams {
mode: Option<String>,
}
fn compact_postcode_query(query: &str) -> String {
query
.chars()
.filter(|ch| !ch.is_whitespace())
.map(|ch| ch.to_ascii_uppercase())
.collect()
}
fn looks_like_postcode_prefix(query: &str) -> bool {
let compact = compact_postcode_query(query);
if compact.len() < 2 || compact.len() > 7 {
return false;
}
compact
.chars()
.next()
.is_some_and(|ch| ch.is_ascii_alphabetic())
&& compact.chars().all(|ch| ch.is_ascii_alphanumeric())
&& compact.chars().any(|ch| ch.is_ascii_digit())
}
fn postcode_starts_with_compact(postcode: &str, compact_query: &str) -> bool {
let mut query_chars = compact_query.chars();
let mut current = query_chars.next();
if current.is_none() {
return false;
}
for postcode_char in postcode.chars() {
if postcode_char.is_whitespace() {
continue;
}
match current {
Some(query_char) if postcode_char.to_ascii_uppercase() == query_char => {
current = query_chars.next();
if current.is_none() {
return true;
}
}
_ => return false,
}
}
current.is_none()
}
pub async fn get_places(
State(shared): State<Arc<SharedState>>,
Query(params): Query<PlacesParams>,
@ -51,31 +110,39 @@ pub async fn get_places(
let places = tokio::task::spawn_blocking(move || {
let t0 = std::time::Instant::now();
let query_lower = query.to_lowercase();
let query_search = normalize_search_text(&query);
let pd = &state.place_data;
let od = &state.outcode_data;
let postcode_data = &state.postcode_data;
let tt_store = &state.travel_time_store;
let property_data = &state.data;
// Linear scan — ~50-100k rows, <1ms
// Tuple: (row_idx, is_exact, is_prefix, type_rank, population, name_len, slug)
let mut matches: Vec<(usize, bool, bool, u8, u32, usize, String)> = pd
.name_lower
.name_search
.iter()
.enumerate()
.filter_map(|(idx, name)| {
if !name.contains(&query_lower) {
.filter_map(|(idx, search_text)| {
if query_search.is_empty() || !search_text.contains(&query_search) {
return None;
}
let slug = slugify(&pd.name[idx]);
// If mode filter is set, only include places with travel data
// If mode filter is set, keep the historical travel destination set only.
if let Some(ref mode) = mode_filter {
if !tt_store.has_destination(mode, &slug) {
if !pd.travel_destination[idx] || !tt_store.has_destination(mode, &slug) {
return None;
}
}
let is_exact = name.len() == query_lower.len();
let is_prefix = name.starts_with(&query_lower);
let is_exact = search_text
.split(" | ")
.any(|alias| alias == query_search || pd.name_lower[idx] == query_lower);
let is_prefix = search_text
.split(" | ")
.any(|alias| alias.starts_with(&query_search))
|| pd.name_lower[idx].starts_with(&query_lower);
Some((
idx,
is_exact,
@ -153,20 +220,76 @@ pub async fn get_places(
results = outcode_results;
}
let postcodes: Vec<String> = if mode_filter.is_none() && looks_like_postcode_prefix(&query)
{
let compact_query = compact_postcode_query(&query);
postcode_data
.postcodes
.iter()
.filter(|postcode| postcode_starts_with_compact(postcode, &compact_query))
.take(limit)
.cloned()
.collect()
} else {
Vec::new()
};
let addresses: Vec<AddressResult> = if mode_filter.is_none() {
property_data
.search_addresses(&query, limit)
.into_iter()
.map(|row| AddressResult {
address: property_data.address(row).trim().to_string(),
postcode: property_data.postcode(row).to_string(),
lat: property_data.lat[row],
lon: property_data.lon[row],
})
.collect()
} else {
Vec::new()
};
let elapsed = t0.elapsed();
info!(
query = query.as_str(),
results = results.len(),
postcodes = postcodes.len(),
addresses = addresses.len(),
scanned = pd.name_lower.len(),
mode = mode_filter.as_deref().unwrap_or("-"),
ms = format_args!("{:.1}", elapsed.as_secs_f64() * 1000.0),
"GET /api/places"
);
results
(results, postcodes, addresses)
})
.await
.map_err(|error| (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()))?;
Ok(Json(PlacesResponse { places }))
Ok(Json(PlacesResponse {
places: places.0,
postcodes: places.1,
addresses: places.2,
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_postcode_prefixes() {
assert!(looks_like_postcode_prefix("EC2R"));
assert!(looks_like_postcode_prefix("sw1a 1"));
assert!(looks_like_postcode_prefix("M4"));
assert!(!looks_like_postcode_prefix("London"));
assert!(!looks_like_postcode_prefix("E"));
}
#[test]
fn postcode_prefix_match_ignores_spaces() {
assert!(postcode_starts_with_compact("EC2R 8AH", "EC2R8"));
assert!(postcode_starts_with_compact("SW1A 1AA", "SW1A1"));
assert!(!postcode_starts_with_compact("SW1A 1AA", "SW1A2"));
}
}

View file

@ -22,6 +22,8 @@ pub struct PostcodePropertiesParams {
pub filters: Option<String>,
pub limit: Option<usize>,
pub offset: Option<usize>,
/// Exact address to rank first when opening properties from address search.
pub focus_address: Option<String>,
/// Share-link code; grants bbox-scoped access for unlicensed users.
pub share: Option<String>,
}
@ -67,6 +69,12 @@ pub async fn get_postcode_properties(
let filters_str = params.filters;
let postcode_str = normalized;
let focus_address = params
.focus_address
.as_deref()
.map(str::trim)
.filter(|address| !address.is_empty())
.map(str::to_ascii_lowercase);
let result = tokio::task::spawn_blocking(move || {
let t0 = std::time::Instant::now();
@ -100,7 +108,20 @@ pub async fn get_postcode_properties(
}
});
matching_rows.sort_unstable_by_key(|&row| state.data.address(row).trim().is_empty());
matching_rows.sort_unstable_by(|&left, &right| {
let left_address = state.data.address(left).trim();
let right_address = state.data.address(right).trim();
let left_focused = focus_address
.as_ref()
.is_some_and(|address| left_address.eq_ignore_ascii_case(address));
let right_focused = focus_address
.as_ref()
.is_some_and(|address| right_address.eq_ignore_ascii_case(address));
right_focused
.cmp(&left_focused)
.then(left_address.is_empty().cmp(&right_address.is_empty()))
});
let total = matching_rows.len();
let limit = params

View file

@ -54,6 +54,9 @@ pub async fn get_travel_destinations(
.iter()
.enumerate()
.filter_map(|(idx, name)| {
if !pd.travel_destination[idx] {
return None;
}
let slug = slugify(name);
if slug_set.contains(&slug) {
Some((idx, slug, pd.type_rank[idx], pd.population[idx], name.len()))