seems fine
This commit is contained in:
parent
48983e3b4b
commit
7a1696541f
37 changed files with 4999 additions and 1242 deletions
|
|
@ -5,11 +5,6 @@
|
|||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://perfect-postcode.co.uk/dashboard</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://perfect-postcode.co.uk/learn</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
|
|
@ -20,4 +15,59 @@
|
|||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://perfect-postcode.co.uk/property-price-map</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.85</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://perfect-postcode.co.uk/postcode-property-search</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.85</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://perfect-postcode.co.uk/commute-property-search</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.85</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://perfect-postcode.co.uk/school-property-search</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://perfect-postcode.co.uk/postcode-checker</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://perfect-postcode.co.uk/property-search/birmingham</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.75</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://perfect-postcode.co.uk/property-search/manchester</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.75</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://perfect-postcode.co.uk/property-search/bristol</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.75</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://perfect-postcode.co.uk/data-sources</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://perfect-postcode.co.uk/methodology</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://perfect-postcode.co.uk/privacy-security</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,141 @@
|
|||
import { createServer } from 'http';
|
||||
import { readFileSync, writeFileSync, existsSync, statSync } from 'fs';
|
||||
import { join, extname } from 'path';
|
||||
import { readFileSync, writeFileSync, existsSync, statSync, mkdirSync } from 'fs';
|
||||
import { join, extname, dirname } from 'path';
|
||||
import { launch } from 'puppeteer';
|
||||
|
||||
const DIST_DIR = join(import.meta.dirname, '..', 'dist');
|
||||
const INDEX_PATH = join(DIST_DIR, 'index.html');
|
||||
const PUBLIC_URL = 'https://perfect-postcode.co.uk';
|
||||
const OG_PLACEHOLDER = '<meta name="x-og-placeholder" content="__PERFECT_POSTCODE_OG_TAGS__"/>';
|
||||
|
||||
const ROUTES = [
|
||||
{
|
||||
path: '/',
|
||||
output: 'index.html',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
path: '/learn',
|
||||
output: 'learn/index.html',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
path: '/pricing',
|
||||
output: 'pricing/index.html',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
path: '/property-price-map',
|
||||
output: 'property-price-map/index.html',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
path: '/postcode-property-search',
|
||||
output: 'postcode-property-search/index.html',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
path: '/commute-property-search',
|
||||
output: 'commute-property-search/index.html',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
path: '/school-property-search',
|
||||
output: 'school-property-search/index.html',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
path: '/postcode-checker',
|
||||
output: 'postcode-checker/index.html',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
path: '/property-search/birmingham',
|
||||
output: 'property-search/birmingham/index.html',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
path: '/property-search/manchester',
|
||||
output: 'property-search/manchester/index.html',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
path: '/property-search/bristol',
|
||||
output: 'property-search/bristol/index.html',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
path: '/data-sources',
|
||||
output: 'data-sources/index.html',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
path: '/methodology',
|
||||
output: 'methodology/index.html',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
path: '/privacy-security',
|
||||
output: 'privacy-security/index.html',
|
||||
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.',
|
||||
},
|
||||
];
|
||||
|
||||
const FAQ_SCHEMA_ITEMS = [
|
||||
{
|
||||
question: 'Where should I look once the obvious areas are too expensive?',
|
||||
answer:
|
||||
'Set your budget, property type, floor area, commute, schools, crime, noise, broadband, parks, and other must-haves. The map removes postcodes that fail those tests, so overlooked areas can surface before you start searching listings.',
|
||||
},
|
||||
{
|
||||
question: 'What should I do when my search returns too many or too few areas?',
|
||||
answer:
|
||||
'Start with hard limits, then colour the map by a trade-off such as price per sqm, road noise, school score, or commute time. If the map gets too narrow, relax one slider and you can see exactly which compromise opens up more options.',
|
||||
},
|
||||
{
|
||||
question: 'How are the travel times calculated?',
|
||||
answer:
|
||||
'Travel times are precomputed with Conveyal R5, a routing engine used for transport analysis. For each supported destination we route to reachable postcodes over the street and transit network, then store sparse postcode travel-time files for car, cycling, walking, and public transport.',
|
||||
},
|
||||
{
|
||||
question: 'How does the estimated current price algorithm work?',
|
||||
answer:
|
||||
'The estimate starts with the last HM Land Registry sale price, adjusts it to current-market terms using repeat-sales modelling and fallback models, then blends that result with a nearest-neighbour estimate from nearby, recently sold, same-type homes.',
|
||||
},
|
||||
{
|
||||
question: 'Should I use this before or after checking Rightmove?',
|
||||
answer:
|
||||
'Use Perfect Postcode before and alongside listing portals. Rightmove, Zoopla, and OnTheMarket are still where you check live availability, photos, agent contact, viewings, and alerts.',
|
||||
},
|
||||
];
|
||||
|
||||
const MIME_TYPES = {
|
||||
'.html': 'text/html',
|
||||
|
|
@ -13,9 +144,151 @@ const MIME_TYPES = {
|
|||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.mp4': 'video/mp4',
|
||||
'.webm': 'video/webm',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
};
|
||||
|
||||
function escapeAttr(value) {
|
||||
return value
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>');
|
||||
}
|
||||
|
||||
function routeUrl(pathname) {
|
||||
return `${PUBLIC_URL}${pathname === '/' ? '/' : pathname}`;
|
||||
}
|
||||
|
||||
function jsonLd(data) {
|
||||
return `<script type="application/ld+json">${JSON.stringify(data).replaceAll(
|
||||
'<',
|
||||
'\\u003c'
|
||||
)}</script>`;
|
||||
}
|
||||
|
||||
function structuredDataForRoute(route) {
|
||||
const url = routeUrl(route.path);
|
||||
const base = [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
'@id': `${url}#webpage`,
|
||||
url,
|
||||
name: route.title,
|
||||
description: route.description,
|
||||
isPartOf: { '@id': `${PUBLIC_URL}/#website` },
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 1,
|
||||
name: 'Perfect Postcode',
|
||||
item: `${PUBLIC_URL}/`,
|
||||
},
|
||||
...(route.path === '/'
|
||||
? []
|
||||
: [
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 2,
|
||||
name: route.title.split(' - ')[0],
|
||||
item: url,
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
if (route.path === '/') {
|
||||
base.push(
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
'@id': `${PUBLIC_URL}/#organization`,
|
||||
name: 'Perfect Postcode',
|
||||
url: `${PUBLIC_URL}/`,
|
||||
logo: `${PUBLIC_URL}/favicon.svg`,
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
'@id': `${PUBLIC_URL}/#website`,
|
||||
name: 'Perfect Postcode',
|
||||
url: `${PUBLIC_URL}/`,
|
||||
publisher: { '@id': `${PUBLIC_URL}/#organization` },
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'Perfect Postcode',
|
||||
applicationCategory: 'BusinessApplication',
|
||||
operatingSystem: 'Web',
|
||||
url: `${PUBLIC_URL}/`,
|
||||
description: route.description,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (route.path === '/learn') {
|
||||
base.push({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: FAQ_SCHEMA_ITEMS.map((item) => ({
|
||||
'@type': 'Question',
|
||||
name: item.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: item.answer,
|
||||
},
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (route.path === '/pricing') {
|
||||
base.push({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Product',
|
||||
name: 'Perfect Postcode lifetime access',
|
||||
description: route.description,
|
||||
brand: { '@type': 'Brand', name: 'Perfect Postcode' },
|
||||
});
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
function updateHead(indexHtml, route) {
|
||||
const structuredData = structuredDataForRoute(route).map(jsonLd).join('\n ');
|
||||
return indexHtml
|
||||
.replace(/<title>.*?<\/title>/, `<title>${escapeAttr(route.title)}</title>`)
|
||||
.replace(
|
||||
/<meta name="description" content="[^"]*" ?\/?>/,
|
||||
`<meta name="description" content="${escapeAttr(route.description)}" />`
|
||||
)
|
||||
.replace(OG_PLACEHOLDER, `${OG_PLACEHOLDER}\n ${structuredData}`);
|
||||
}
|
||||
|
||||
function cleanBaseIndexHtml(indexHtml) {
|
||||
const withoutStructuredData = indexHtml.replace(
|
||||
/<script type="application\/ld\+json">.*?<\/script>\s*/gs,
|
||||
''
|
||||
);
|
||||
const rootStart = withoutStructuredData.indexOf('<div id="root">');
|
||||
const bodyEnd = withoutStructuredData.indexOf('</body>', rootStart);
|
||||
|
||||
if (rootStart === -1 || bodyEnd === -1) {
|
||||
return withoutStructuredData;
|
||||
}
|
||||
|
||||
return `${withoutStructuredData.slice(0, rootStart)}<div id="root"></div>${withoutStructuredData.slice(bodyEnd)}`;
|
||||
}
|
||||
|
||||
function startServer() {
|
||||
return new Promise((resolve) => {
|
||||
const server = createServer((req, res) => {
|
||||
|
|
@ -53,81 +326,101 @@ async function prerender() {
|
|||
});
|
||||
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
const baseIndexHtml = cleanBaseIndexHtml(readFileSync(INDEX_PATH, 'utf-8'));
|
||||
|
||||
// Intercept API requests to prevent real fetches and retry loops
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
const url = req.url();
|
||||
if (url.includes('/api/features')) {
|
||||
req.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ groups: [] }),
|
||||
for (const route of ROUTES) {
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Intercept API requests to prevent real fetches and retry loops.
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
const url = req.url();
|
||||
if (url.includes('/api/features')) {
|
||||
req.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ groups: [] }),
|
||||
});
|
||||
} else if (url.includes('/api/poi-categories')) {
|
||||
req.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ groups: [] }),
|
||||
});
|
||||
} else if (url.includes('/api/pricing')) {
|
||||
req.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
licensed_count: 120,
|
||||
current_price_pence: 999,
|
||||
tiers: [
|
||||
{ up_to: 50, price_pence: 99, slots: 50 },
|
||||
{ up_to: 150, price_pence: 999, slots: 100 },
|
||||
{ up_to: 250, price_pence: 2999, slots: 100 },
|
||||
{ up_to: 350, price_pence: 4999, slots: 100 },
|
||||
{ up_to: null, price_pence: 9999, slots: 0 },
|
||||
],
|
||||
}),
|
||||
});
|
||||
} else if (url.includes('/api/')) {
|
||||
req.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: '{}',
|
||||
});
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(`http://127.0.0.1:${port}${route.path}`, {
|
||||
waitUntil: 'networkidle0',
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
await page.waitForSelector('h1', { timeout: 10000 });
|
||||
|
||||
// Extract and clean the rendered HTML.
|
||||
const html = await page.evaluate(() => {
|
||||
const root = document.getElementById('root');
|
||||
if (!root) return '';
|
||||
|
||||
// Strip fade-in-visible classes (added by IntersectionObserver effects).
|
||||
root.querySelectorAll('.fade-in-visible').forEach((el) => {
|
||||
el.classList.remove('fade-in-visible');
|
||||
});
|
||||
} else if (url.includes('/api/poi-categories')) {
|
||||
req.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ groups: [] }),
|
||||
|
||||
// Clean canvas elements (dimensions set by ResizeObserver effect).
|
||||
root.querySelectorAll('canvas').forEach((canvas) => {
|
||||
canvas.removeAttribute('width');
|
||||
canvas.removeAttribute('height');
|
||||
canvas.style.removeProperty('width');
|
||||
canvas.style.removeProperty('height');
|
||||
});
|
||||
} else if (url.includes('/api/')) {
|
||||
req.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: '{}',
|
||||
});
|
||||
} else {
|
||||
req.continue();
|
||||
|
||||
return root.innerHTML;
|
||||
});
|
||||
|
||||
if (!html || html.length < 100) {
|
||||
throw new Error(`Prerender produced too little HTML for ${route.path}`);
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(`http://127.0.0.1:${port}/`, {
|
||||
waitUntil: 'networkidle0',
|
||||
timeout: 30000,
|
||||
});
|
||||
const updated = updateHead(baseIndexHtml, route).replace(
|
||||
'<div id="root"></div>',
|
||||
`<div id="root">${html}</div>`
|
||||
);
|
||||
|
||||
// Wait for the home page heading to render
|
||||
await page.waitForSelector('h1', { timeout: 10000 });
|
||||
if (updated === baseIndexHtml) {
|
||||
throw new Error('Could not find <div id="root"></div> in index.html');
|
||||
}
|
||||
|
||||
// Extract and clean the rendered HTML
|
||||
const html = await page.evaluate(() => {
|
||||
const root = document.getElementById('root');
|
||||
if (!root) return '';
|
||||
|
||||
// Strip fade-in-visible classes (added by IntersectionObserver effects)
|
||||
root.querySelectorAll('.fade-in-visible').forEach((el) => {
|
||||
el.classList.remove('fade-in-visible');
|
||||
});
|
||||
|
||||
// Clean canvas elements (dimensions set by ResizeObserver effect)
|
||||
root.querySelectorAll('canvas').forEach((canvas) => {
|
||||
canvas.removeAttribute('width');
|
||||
canvas.removeAttribute('height');
|
||||
canvas.style.removeProperty('width');
|
||||
canvas.style.removeProperty('height');
|
||||
});
|
||||
|
||||
return root.innerHTML;
|
||||
});
|
||||
|
||||
if (!html || html.length < 100) {
|
||||
throw new Error('Prerender produced too little HTML — something went wrong');
|
||||
const outputPath = join(DIST_DIR, route.output);
|
||||
mkdirSync(dirname(outputPath), { recursive: true });
|
||||
writeFileSync(outputPath, updated);
|
||||
await page.close();
|
||||
console.log(`Prerendered ${route.path} (${html.length} chars) into ${route.output}`);
|
||||
}
|
||||
|
||||
// Inject into dist/index.html
|
||||
const indexHtml = readFileSync(INDEX_PATH, 'utf-8');
|
||||
const updated = indexHtml.replace(
|
||||
'<div id="root"></div>',
|
||||
`<div id="root">${html}</div>`
|
||||
);
|
||||
|
||||
if (updated === indexHtml) {
|
||||
throw new Error('Could not find <div id="root"></div> in index.html');
|
||||
}
|
||||
|
||||
writeFileSync(INDEX_PATH, updated);
|
||||
console.log(`Prerendered ${html.length} chars into dist/index.html`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
server.close();
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import MapPage, { type ExportState } from './components/map/MapPage';
|
||||
import PricingPage from './components/pricing/PricingPage';
|
||||
import HomePage from './components/home/HomePage';
|
||||
import LearnPage from './components/learn/LearnPage';
|
||||
import AccountPage, { SavedPage, InvitesPage } from './components/account/AccountPage';
|
||||
import InvitePage from './components/invite/InvitePage';
|
||||
import { lazy, Suspense, useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import type { ExportState } from './components/map/MapPage';
|
||||
import {
|
||||
getSeoContentPage,
|
||||
getSeoLandingPage,
|
||||
isSeoContentKey,
|
||||
isSeoLandingKey,
|
||||
SEO_CONTENT_PATHS,
|
||||
SEO_LANDING_PATHS,
|
||||
type SeoContentKey,
|
||||
type SeoLandingKey,
|
||||
} from './lib/seoRoutes';
|
||||
import Header, { type Page } from './components/ui/Header';
|
||||
import AuthModal from './components/ui/AuthModal';
|
||||
import SaveSearchModal from './components/ui/SaveSearchModal';
|
||||
import LicenseSuccessModal from './components/ui/LicenseSuccessModal';
|
||||
import type { FeatureMeta, FeatureGroup, POICategoriesResponse, POICategoryGroup } from './types';
|
||||
import { fetchWithRetry, apiUrl } from './lib/api';
|
||||
import { trackEvent } from './lib/analytics';
|
||||
|
|
@ -28,6 +30,28 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
const HomePage = lazy(() => import('./components/home/HomePage'));
|
||||
const PricingPage = lazy(() => import('./components/pricing/PricingPage'));
|
||||
const LearnPage = lazy(() => import('./components/learn/LearnPage'));
|
||||
const SeoLandingPage = lazy(() => import('./components/landing/SeoLandingPage'));
|
||||
const SeoContentPage = lazy(() => import('./components/landing/SeoContentPage'));
|
||||
const AccountPage = lazy(() => import('./components/account/AccountPage'));
|
||||
const SavedPage = lazy(() =>
|
||||
import('./components/account/AccountPage').then((module) => ({ default: module.SavedPage }))
|
||||
);
|
||||
const InvitesPage = lazy(() =>
|
||||
import('./components/account/AccountPage').then((module) => ({ default: module.InvitesPage }))
|
||||
);
|
||||
const InvitePage = lazy(() => import('./components/invite/InvitePage'));
|
||||
const MapPage = lazy(() => import('./components/map/MapPage'));
|
||||
const AuthModal = lazy(() => import('./components/ui/AuthModal'));
|
||||
const SaveSearchModal = lazy(() => import('./components/ui/SaveSearchModal'));
|
||||
const LicenseSuccessModal = lazy(() => import('./components/ui/LicenseSuccessModal'));
|
||||
|
||||
function PageFallback() {
|
||||
return <div className="flex-1 bg-warm-50 dark:bg-navy-950" />;
|
||||
}
|
||||
|
||||
function pageToPath(page: Page, inviteCode?: string): string {
|
||||
switch (page) {
|
||||
case 'dashboard':
|
||||
|
|
@ -36,6 +60,19 @@ function pageToPath(page: Page, inviteCode?: string): string {
|
|||
return '/learn';
|
||||
case 'pricing':
|
||||
return '/pricing';
|
||||
case 'property-price-map':
|
||||
case 'postcode-property-search':
|
||||
case 'commute-property-search':
|
||||
case 'school-property-search':
|
||||
case 'postcode-checker':
|
||||
return SEO_LANDING_PATHS[page];
|
||||
case 'birmingham-property-search':
|
||||
case 'manchester-property-search':
|
||||
case 'bristol-property-search':
|
||||
case 'data-sources':
|
||||
case 'methodology':
|
||||
case 'privacy-security':
|
||||
return SEO_CONTENT_PATHS[page];
|
||||
case 'saved':
|
||||
return '/saved';
|
||||
case 'invites':
|
||||
|
|
@ -55,6 +92,10 @@ function pathToPage(pathname: string): { page: Page; inviteCode?: string } | nul
|
|||
if (pathname === '/invites') return { page: 'invites' };
|
||||
if (pathname === '/learn') return { page: 'learn' };
|
||||
if (pathname === '/pricing') return { page: 'pricing' };
|
||||
const seoLandingPage = getSeoLandingPage(pathname);
|
||||
if (seoLandingPage) return { page: seoLandingPage };
|
||||
const seoContentPage = getSeoContentPage(pathname);
|
||||
if (seoContentPage) return { page: seoContentPage };
|
||||
if (pathname === '/account') return { page: 'account' };
|
||||
if (pathname === '/support') return { page: 'learn' };
|
||||
if (pathname.startsWith('/invite/')) {
|
||||
|
|
@ -65,6 +106,14 @@ function pathToPage(pathname: string): { page: Page; inviteCode?: string } | nul
|
|||
return null;
|
||||
}
|
||||
|
||||
function isSeoLandingPage(page: Page): page is SeoLandingKey {
|
||||
return isSeoLandingKey(page);
|
||||
}
|
||||
|
||||
function isSeoContentPage(page: Page): page is SeoContentKey {
|
||||
return isSeoContentKey(page);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const urlState = useMemo(() => parseUrlState(), []);
|
||||
const [mapUrlState, setMapUrlState] = useState(urlState);
|
||||
|
|
@ -271,37 +320,41 @@ export default function App() {
|
|||
|
||||
if ((isScreenshotMode || isOgMode) && inviteCode) {
|
||||
return (
|
||||
<InvitePage
|
||||
code={inviteCode}
|
||||
user={null}
|
||||
theme={theme}
|
||||
screenshotMode
|
||||
onLoginClick={() => {}}
|
||||
onRegisterClick={() => {}}
|
||||
onLicenseGranted={() => {}}
|
||||
/>
|
||||
<Suspense fallback={<PageFallback />}>
|
||||
<InvitePage
|
||||
code={inviteCode}
|
||||
user={null}
|
||||
theme={theme}
|
||||
screenshotMode
|
||||
onLoginClick={() => {}}
|
||||
onRegisterClick={() => {}}
|
||||
onLicenseGranted={() => {}}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
if (isScreenshotMode) {
|
||||
return (
|
||||
<MapPage
|
||||
features={features}
|
||||
poiCategoryGroups={poiCategoryGroups}
|
||||
initialFilters={urlState.filters || {}}
|
||||
initialViewState={initialViewState}
|
||||
initialPOICategories={urlState.poiCategories || new Set()}
|
||||
initialTab={urlState.tab || 'area'}
|
||||
initialLoading={initialLoading}
|
||||
theme={theme}
|
||||
pendingInfoFeature={null}
|
||||
onClearPendingInfoFeature={() => {}}
|
||||
onNavigateTo={() => {}}
|
||||
screenshotMode
|
||||
ogMode={isOgMode}
|
||||
initialTravelTime={urlState.travelTime}
|
||||
shareCode={urlState.share}
|
||||
/>
|
||||
<Suspense fallback={<PageFallback />}>
|
||||
<MapPage
|
||||
features={features}
|
||||
poiCategoryGroups={poiCategoryGroups}
|
||||
initialFilters={urlState.filters || {}}
|
||||
initialViewState={initialViewState}
|
||||
initialPOICategories={urlState.poiCategories || new Set()}
|
||||
initialTab={urlState.tab || 'area'}
|
||||
initialLoading={initialLoading}
|
||||
theme={theme}
|
||||
pendingInfoFeature={null}
|
||||
onClearPendingInfoFeature={() => {}}
|
||||
onNavigateTo={() => {}}
|
||||
screenshotMode
|
||||
ogMode={isOgMode}
|
||||
initialTravelTime={urlState.travelTime}
|
||||
shareCode={urlState.share}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -328,137 +381,145 @@ export default function App() {
|
|||
onLogout={logout}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
{activePage === 'home' ? (
|
||||
<HomePage
|
||||
onOpenDashboard={() => navigateTo('dashboard')}
|
||||
onOpenPricing={() => navigateTo('pricing')}
|
||||
theme={theme}
|
||||
hidePricing={user?.subscription === 'licensed' || user?.isAdmin}
|
||||
/>
|
||||
) : activePage === 'pricing' && !(user?.subscription === 'licensed' || user?.isAdmin) ? (
|
||||
<PricingPage
|
||||
onOpenDashboard={() => navigateTo('dashboard')}
|
||||
user={user}
|
||||
onLoginClick={() => {
|
||||
setAuthModalTab('login');
|
||||
setShowAuthModal(true);
|
||||
}}
|
||||
onRegisterClick={() => {
|
||||
setAuthModalTab('register');
|
||||
setShowAuthModal(true);
|
||||
}}
|
||||
/>
|
||||
) : activePage === 'learn' ? (
|
||||
<LearnPage />
|
||||
) : activePage === 'saved' && user ? (
|
||||
<SavedPage
|
||||
searches={savedSearches.searches}
|
||||
searchesLoading={savedSearches.loading}
|
||||
onDeleteSearch={savedSearches.deleteSearch}
|
||||
onUpdateSearchNotes={savedSearches.updateSearchNotes}
|
||||
onUpdateSearchName={savedSearches.updateSearchName}
|
||||
onOpenSearch={(params) => {
|
||||
window.location.href = `/dashboard?${params}`;
|
||||
}}
|
||||
savedProperties={savedProperties.properties}
|
||||
propertiesLoading={savedProperties.loading}
|
||||
onDeleteProperty={savedProperties.deleteProperty}
|
||||
onUpdatePropertyNotes={savedProperties.updatePropertyNotes}
|
||||
onOpenProperty={(postcode) => {
|
||||
window.location.href = `/dashboard?pc=${encodeURIComponent(postcode)}`;
|
||||
}}
|
||||
/>
|
||||
) : activePage === 'invites' && user ? (
|
||||
<InvitesPage user={user} />
|
||||
) : activePage === 'account' && user ? (
|
||||
<AccountPage user={user} onRefreshAuth={refreshAuth} />
|
||||
) : activePage === 'invite' && inviteCode ? (
|
||||
<InvitePage
|
||||
code={inviteCode}
|
||||
user={user}
|
||||
theme={theme}
|
||||
onLoginClick={() => {
|
||||
setAuthModalTab('login');
|
||||
setShowAuthModal(true);
|
||||
}}
|
||||
onRegisterClick={() => {
|
||||
setAuthModalTab('register');
|
||||
setShowAuthModal(true);
|
||||
}}
|
||||
onLicenseGranted={() => {
|
||||
setShowLicenseSuccess(true);
|
||||
refreshAuth();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<MapPage
|
||||
features={features}
|
||||
poiCategoryGroups={poiCategoryGroups}
|
||||
initialFilters={mapUrlState.filters || {}}
|
||||
initialViewState={initialViewState}
|
||||
initialPOICategories={mapUrlState.poiCategories || new Set()}
|
||||
initialTab={mapUrlState.tab || 'area'}
|
||||
initialLoading={initialLoading}
|
||||
theme={theme}
|
||||
pendingInfoFeature={pendingInfoFeature}
|
||||
onClearPendingInfoFeature={() => setPendingInfoFeature(null)}
|
||||
onNavigateTo={navigateTo}
|
||||
onExportStateChange={setExportState}
|
||||
isMobile={isMobile}
|
||||
initialTravelTime={mapUrlState.travelTime}
|
||||
initialPostcode={mapUrlState.postcode}
|
||||
shareCode={mapUrlState.share}
|
||||
user={user}
|
||||
onLoginClick={() => {
|
||||
setAuthModalTab('login');
|
||||
setShowAuthModal(true);
|
||||
}}
|
||||
onRegisterClick={() => {
|
||||
setAuthModalTab('register');
|
||||
setShowAuthModal(true);
|
||||
}}
|
||||
onSaveProperty={user ? savedProperties.saveProperty : undefined}
|
||||
onUnsaveProperty={user ? savedProperties.deleteProperty : undefined}
|
||||
isPropertySaved={user ? savedProperties.isPropertySaved : undefined}
|
||||
getSavedPropertyId={user ? savedProperties.getSavedPropertyId : undefined}
|
||||
deferTutorial={showLicenseSuccess}
|
||||
onSaveSearch={user ? savedSearches.saveSearch : undefined}
|
||||
savingSearch={savedSearches.saving}
|
||||
/>
|
||||
)}
|
||||
{showAuthModal && (
|
||||
<AuthModal
|
||||
onClose={() => setShowAuthModal(false)}
|
||||
onLogin={login}
|
||||
onRegister={register}
|
||||
onOAuthLogin={loginWithOAuth}
|
||||
onForgotPassword={requestPasswordReset}
|
||||
loading={authLoading}
|
||||
error={authError}
|
||||
onClearError={clearError}
|
||||
initialTab={authModalTab}
|
||||
/>
|
||||
)}
|
||||
{showSaveModal && (
|
||||
<SaveSearchModal
|
||||
onClose={() => setShowSaveModal(false)}
|
||||
onSave={savedSearches.saveSearch}
|
||||
onViewSearches={() => {
|
||||
setShowSaveModal(false);
|
||||
navigateTo('saved');
|
||||
}}
|
||||
saving={savedSearches.saving}
|
||||
error={savedSearches.error}
|
||||
/>
|
||||
)}
|
||||
{showLicenseSuccess && (
|
||||
<LicenseSuccessModal
|
||||
onClose={() => {
|
||||
setShowLicenseSuccess(false);
|
||||
navigateTo('dashboard');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Suspense fallback={<PageFallback />}>
|
||||
{activePage === 'home' ? (
|
||||
<HomePage
|
||||
onOpenDashboard={() => navigateTo('dashboard')}
|
||||
onOpenPricing={() => navigateTo('pricing')}
|
||||
theme={theme}
|
||||
hidePricing={user?.subscription === 'licensed' || user?.isAdmin}
|
||||
/>
|
||||
) : activePage === 'pricing' && !(user?.subscription === 'licensed' || user?.isAdmin) ? (
|
||||
<PricingPage
|
||||
onOpenDashboard={() => navigateTo('dashboard')}
|
||||
user={user}
|
||||
onLoginClick={() => {
|
||||
setAuthModalTab('login');
|
||||
setShowAuthModal(true);
|
||||
}}
|
||||
onRegisterClick={() => {
|
||||
setAuthModalTab('register');
|
||||
setShowAuthModal(true);
|
||||
}}
|
||||
/>
|
||||
) : activePage === 'learn' ? (
|
||||
<LearnPage />
|
||||
) : isSeoLandingPage(activePage) ? (
|
||||
<SeoLandingPage pageKey={activePage} onOpenDashboard={() => navigateTo('dashboard')} />
|
||||
) : isSeoContentPage(activePage) ? (
|
||||
<SeoContentPage pageKey={activePage} onOpenDashboard={() => navigateTo('dashboard')} />
|
||||
) : activePage === 'saved' && user ? (
|
||||
<SavedPage
|
||||
searches={savedSearches.searches}
|
||||
searchesLoading={savedSearches.loading}
|
||||
onDeleteSearch={savedSearches.deleteSearch}
|
||||
onUpdateSearchNotes={savedSearches.updateSearchNotes}
|
||||
onUpdateSearchName={savedSearches.updateSearchName}
|
||||
onOpenSearch={(params) => {
|
||||
window.location.href = `/dashboard?${params}`;
|
||||
}}
|
||||
savedProperties={savedProperties.properties}
|
||||
propertiesLoading={savedProperties.loading}
|
||||
onDeleteProperty={savedProperties.deleteProperty}
|
||||
onUpdatePropertyNotes={savedProperties.updatePropertyNotes}
|
||||
onOpenProperty={(postcode) => {
|
||||
window.location.href = `/dashboard?pc=${encodeURIComponent(postcode)}`;
|
||||
}}
|
||||
/>
|
||||
) : activePage === 'invites' && user ? (
|
||||
<InvitesPage user={user} />
|
||||
) : activePage === 'account' && user ? (
|
||||
<AccountPage user={user} onRefreshAuth={refreshAuth} />
|
||||
) : activePage === 'invite' && inviteCode ? (
|
||||
<InvitePage
|
||||
code={inviteCode}
|
||||
user={user}
|
||||
theme={theme}
|
||||
onLoginClick={() => {
|
||||
setAuthModalTab('login');
|
||||
setShowAuthModal(true);
|
||||
}}
|
||||
onRegisterClick={() => {
|
||||
setAuthModalTab('register');
|
||||
setShowAuthModal(true);
|
||||
}}
|
||||
onLicenseGranted={() => {
|
||||
setShowLicenseSuccess(true);
|
||||
refreshAuth();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<MapPage
|
||||
features={features}
|
||||
poiCategoryGroups={poiCategoryGroups}
|
||||
initialFilters={mapUrlState.filters || {}}
|
||||
initialViewState={initialViewState}
|
||||
initialPOICategories={mapUrlState.poiCategories || new Set()}
|
||||
initialTab={mapUrlState.tab || 'area'}
|
||||
initialLoading={initialLoading}
|
||||
theme={theme}
|
||||
pendingInfoFeature={pendingInfoFeature}
|
||||
onClearPendingInfoFeature={() => setPendingInfoFeature(null)}
|
||||
onNavigateTo={navigateTo}
|
||||
onExportStateChange={setExportState}
|
||||
isMobile={isMobile}
|
||||
initialTravelTime={mapUrlState.travelTime}
|
||||
initialPostcode={mapUrlState.postcode}
|
||||
shareCode={mapUrlState.share}
|
||||
user={user}
|
||||
onLoginClick={() => {
|
||||
setAuthModalTab('login');
|
||||
setShowAuthModal(true);
|
||||
}}
|
||||
onRegisterClick={() => {
|
||||
setAuthModalTab('register');
|
||||
setShowAuthModal(true);
|
||||
}}
|
||||
onSaveProperty={user ? savedProperties.saveProperty : undefined}
|
||||
onUnsaveProperty={user ? savedProperties.deleteProperty : undefined}
|
||||
isPropertySaved={user ? savedProperties.isPropertySaved : undefined}
|
||||
getSavedPropertyId={user ? savedProperties.getSavedPropertyId : undefined}
|
||||
deferTutorial={showLicenseSuccess}
|
||||
onSaveSearch={user ? savedSearches.saveSearch : undefined}
|
||||
savingSearch={savedSearches.saving}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
<Suspense fallback={null}>
|
||||
{showAuthModal && (
|
||||
<AuthModal
|
||||
onClose={() => setShowAuthModal(false)}
|
||||
onLogin={login}
|
||||
onRegister={register}
|
||||
onOAuthLogin={loginWithOAuth}
|
||||
onForgotPassword={requestPasswordReset}
|
||||
loading={authLoading}
|
||||
error={authError}
|
||||
onClearError={clearError}
|
||||
initialTab={authModalTab}
|
||||
/>
|
||||
)}
|
||||
{showSaveModal && (
|
||||
<SaveSearchModal
|
||||
onClose={() => setShowSaveModal(false)}
|
||||
onSave={savedSearches.saveSearch}
|
||||
onViewSearches={() => {
|
||||
setShowSaveModal(false);
|
||||
navigateTo('saved');
|
||||
}}
|
||||
saving={savedSearches.saving}
|
||||
error={savedSearches.error}
|
||||
/>
|
||||
)}
|
||||
{showLicenseSuccess && (
|
||||
<LicenseSuccessModal
|
||||
onClose={() => {
|
||||
setShowLicenseSuccess(false);
|
||||
navigateTo('dashboard');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ interface HexConfig {
|
|||
size: number;
|
||||
opacity: number;
|
||||
top: number;
|
||||
left: number;
|
||||
driftDuration: number;
|
||||
bobDuration: number;
|
||||
bobAmount: number;
|
||||
|
|
@ -21,6 +22,7 @@ function generateHexes(): HexConfig[] {
|
|||
size: 10 + Math.random() * 32,
|
||||
opacity: 0.06 + Math.random() * 0.18,
|
||||
top: Math.random() * 100,
|
||||
left: Math.random() * 100,
|
||||
driftDuration,
|
||||
bobDuration: 6 + Math.random() * 8,
|
||||
bobAmount: 8 + Math.random() * 30,
|
||||
|
|
@ -31,7 +33,13 @@ function generateHexes(): HexConfig[] {
|
|||
return hexes;
|
||||
}
|
||||
|
||||
export default function HexCanvas({ isDark = false }: { isDark?: boolean }) {
|
||||
export default function HexCanvas({
|
||||
isDark = false,
|
||||
animated = true,
|
||||
}: {
|
||||
isDark?: boolean;
|
||||
animated?: boolean;
|
||||
}) {
|
||||
const hexes = useMemo(generateHexes, []);
|
||||
|
||||
return (
|
||||
|
|
@ -42,7 +50,14 @@ export default function HexCanvas({ isDark = false }: { isDark?: boolean }) {
|
|||
className="absolute"
|
||||
style={{
|
||||
top: `${hex.top}%`,
|
||||
animation: `hex-drift ${hex.driftDuration}s linear ${hex.delay}s infinite${hex.reverse ? ' reverse' : ''}`,
|
||||
...(animated
|
||||
? {
|
||||
animation: `hex-drift ${hex.driftDuration}s linear ${hex.delay}s infinite${hex.reverse ? ' reverse' : ''}`,
|
||||
}
|
||||
: {
|
||||
left: `${hex.left}%`,
|
||||
transform: `translate(-50%, -50%) rotate(${hex.delay * 12}deg)`,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
|
@ -51,9 +66,9 @@ export default function HexCanvas({ isDark = false }: { isDark?: boolean }) {
|
|||
{
|
||||
width: hex.size,
|
||||
height: (hex.size * 2) / Math.sqrt(3),
|
||||
opacity: hex.opacity * (isDark ? 0.6 : 1),
|
||||
opacity: hex.opacity * (isDark ? 0.45 : 0.6),
|
||||
clipPath: 'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)',
|
||||
animation: `hex-bob ${hex.bobDuration}s ease-in-out infinite`,
|
||||
animation: animated ? `hex-bob ${hex.bobDuration}s ease-in-out infinite` : undefined,
|
||||
'--bob': `${hex.bobAmount}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -203,7 +203,7 @@ export default function InvitePage({
|
|||
{`\u00A3${(Math.round(pricePence * 0.7) / 100).toFixed(2)}`}
|
||||
</span>
|
||||
<span className="text-warm-500 dark:text-warm-400 ml-2 text-3xl">
|
||||
{t('upgrade.once')}
|
||||
{t('pricingPage.lifetime')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -301,7 +301,9 @@ export default function InvitePage({
|
|||
<span className="text-3xl font-extrabold text-teal-600 dark:text-teal-400">
|
||||
{`\u00A3${(Math.round(pricePence * 0.7) / 100).toFixed(2)}`}
|
||||
</span>
|
||||
<span className="text-warm-500 dark:text-warm-400 ml-1">{t('upgrade.once')}</span>
|
||||
<span className="text-warm-500 dark:text-warm-400 ml-1">
|
||||
{t('pricingPage.lifetime')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -260,6 +260,9 @@ export default function LearnPage() {
|
|||
<>
|
||||
<div className="flex-1">
|
||||
<div className="max-w-5xl mx-auto px-6 py-6">
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-warm-900 dark:text-warm-100 mb-3">
|
||||
{t('learnPage.dataSources')}
|
||||
</h1>
|
||||
<p className="text-warm-600 dark:text-warm-400 mb-6">
|
||||
{t('learnPage.dataSourcesIntro', { count: DATA_SOURCE_DEFS.length })}
|
||||
</p>
|
||||
|
|
@ -369,6 +372,9 @@ export default function LearnPage() {
|
|||
</>
|
||||
) : tab === 'faq' ? (
|
||||
<div className="max-w-3xl mx-auto px-6 py-6 w-full">
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-warm-900 dark:text-warm-100 mb-3">
|
||||
{t('learnPage.faq')}
|
||||
</h1>
|
||||
<p className="text-warm-600 dark:text-warm-400 mb-6">{t('learnPage.faqIntro')}</p>
|
||||
<div className="space-y-8">
|
||||
{FAQ_SECTIONS.map((section) => (
|
||||
|
|
@ -387,6 +393,9 @@ export default function LearnPage() {
|
|||
</div>
|
||||
) : (
|
||||
<div className="max-w-2xl mx-auto px-6 py-6 w-full">
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-warm-900 dark:text-warm-100 mb-3">
|
||||
{t('learnPage.support')}
|
||||
</h1>
|
||||
<p className="text-warm-600 dark:text-warm-400 mb-6">{t('learnPage.supportIntro')}</p>
|
||||
<div className="bg-white dark:bg-warm-800 rounded-xl border border-warm-200 dark:border-warm-700 p-6 text-center">
|
||||
<p className="text-warm-600 dark:text-warm-300 mb-2">{t('accountPage.needHelp')}</p>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ interface FeatureBrowserProps {
|
|||
onClearOpenInfoFeature?: () => void;
|
||||
travelTimeEntries: TravelTimeEntry[];
|
||||
onAddTravelTimeEntry: (mode: TransportMode) => void;
|
||||
isLicensed: boolean;
|
||||
}
|
||||
|
||||
export default function FeatureBrowser({
|
||||
|
|
@ -47,7 +46,6 @@ export default function FeatureBrowser({
|
|||
onClearOpenInfoFeature,
|
||||
travelTimeEntries: _travelTimeEntries,
|
||||
onAddTravelTimeEntry,
|
||||
isLicensed,
|
||||
}: FeatureBrowserProps) {
|
||||
const { t } = useTranslation();
|
||||
const modes = useTranslatedModes();
|
||||
|
|
@ -107,6 +105,11 @@ export default function FeatureBrowser({
|
|||
onChange={setSearch}
|
||||
placeholder={t('filters.searchFeatures')}
|
||||
/>
|
||||
{!search && (
|
||||
<p className="mt-2 px-1 text-xs leading-relaxed text-warm-500 dark:text-warm-400">
|
||||
{t('filters.chooseFilters')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{mergedGrouped.map((group) => {
|
||||
|
|
@ -200,10 +203,6 @@ export default function FeatureBrowser({
|
|||
description={search ? t('filters.tryDifferentSearch') : t('filters.removeFilterHint')}
|
||||
className="px-3 py-4"
|
||||
/>
|
||||
) : isLicensed ? (
|
||||
<p className="flex-1 flex items-center justify-center px-4 py-6 text-xs text-center text-warm-400 dark:text-warm-500">
|
||||
{t('filters.chooseFilters')}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{infoFeature && (
|
||||
|
|
|
|||
|
|
@ -1082,7 +1082,6 @@ export default memo(function Filters({
|
|||
onClearOpenInfoFeature={onClearOpenInfoFeature}
|
||||
travelTimeEntries={travelTimeEntries}
|
||||
onAddTravelTimeEntry={handleAddTravelTimeAndScroll}
|
||||
isLicensed={isLicensed}
|
||||
/>
|
||||
{!isLicensed && (
|
||||
<div className="shrink-0 flex flex-col items-center px-5 pt-4 pb-0 border-t border-warm-200 dark:border-warm-700">
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ interface JourneyInstructionsProps {
|
|||
entries: TravelTimeEntry[];
|
||||
/** When set, shown as a subtitle (e.g. the central postcode for a hexagon) */
|
||||
label?: string;
|
||||
/** Preloaded journey rows, useful for static demos that should not call the API. */
|
||||
presetJourneys?: JourneyInstructionPreset[];
|
||||
className?: string;
|
||||
showGoogleMapsLink?: boolean;
|
||||
}
|
||||
|
||||
interface JourneyData {
|
||||
|
|
@ -24,6 +28,16 @@ interface JourneyData {
|
|||
loading: boolean;
|
||||
}
|
||||
|
||||
export interface JourneyInstructionPreset {
|
||||
slug: string;
|
||||
label: string;
|
||||
legs: JourneyLeg[] | null;
|
||||
/** Median (50th percentile) total travel time, including waiting. */
|
||||
minutes: number | null;
|
||||
/** Best-case (5th percentile) total travel time. */
|
||||
bestMinutes?: number | null;
|
||||
}
|
||||
|
||||
// Official TfL line colors + other known London transit
|
||||
const ROUTE_COLORS: Record<string, { color: string; darkText?: boolean }> = {
|
||||
Bakerloo: { color: '#B36305' },
|
||||
|
|
@ -164,14 +178,23 @@ export default function JourneyInstructions({
|
|||
postcode,
|
||||
entries,
|
||||
label,
|
||||
presetJourneys,
|
||||
className,
|
||||
showGoogleMapsLink = true,
|
||||
}: JourneyInstructionsProps) {
|
||||
const { t } = useTranslation();
|
||||
const [journeys, setJourneys] = useState<JourneyData[]>([]);
|
||||
|
||||
// Only transit entries with a destination set
|
||||
const transitEntries = entries.filter((e) => e.mode === 'transit' && e.slug !== '');
|
||||
const hasPresetJourneys = Boolean(presetJourneys?.length);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasPresetJourneys) {
|
||||
setJourneys([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (transitEntries.length === 0) {
|
||||
setJourneys([]);
|
||||
return;
|
||||
|
|
@ -227,18 +250,29 @@ export default function JourneyInstructions({
|
|||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [postcode, transitEntries.map((e) => e.slug).join(',')]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [postcode, hasPresetJourneys, transitEntries.map((e) => e.slug).join(',')]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
if (transitEntries.length === 0) return null;
|
||||
if (transitEntries.length === 0 && !hasPresetJourneys) return null;
|
||||
|
||||
const displayedJourneys: JourneyData[] = hasPresetJourneys
|
||||
? (presetJourneys ?? []).map((journey) => ({
|
||||
slug: journey.slug,
|
||||
label: journey.label,
|
||||
legs: journey.legs,
|
||||
minutes: journey.minutes,
|
||||
bestMinutes: journey.bestMinutes ?? null,
|
||||
loading: false,
|
||||
}))
|
||||
: journeys;
|
||||
|
||||
return (
|
||||
<div className="mx-3 mt-2 space-y-2">
|
||||
<div className={className ?? 'mx-3 mt-2 space-y-2'}>
|
||||
{label && (
|
||||
<div className="text-xs text-warm-500 dark:text-warm-400">
|
||||
{t('areaPane.journeysFrom', { label })}
|
||||
</div>
|
||||
)}
|
||||
{journeys.map((j) => {
|
||||
{displayedJourneys.map((j) => {
|
||||
const displayLegs = j.legs ? invertLegs(j.legs) : null;
|
||||
const legSum = j.legs ? j.legs.reduce((sum, l) => sum + l.minutes, 0) : 0;
|
||||
const totalMin = j.minutes ?? legSum;
|
||||
|
|
@ -267,27 +301,29 @@ export default function JourneyInstructions({
|
|||
{displayLegs.map((leg, i) => (
|
||||
<TimelineLeg key={i} leg={leg} isLast={i === displayLegs.length - 1} />
|
||||
))}
|
||||
<a
|
||||
href={googleMapsUrl(postcode, j.label || j.slug)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-2 flex items-center justify-center gap-1.5 w-full text-[11px] font-medium text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 bg-white dark:bg-warm-900 border border-warm-200 dark:border-warm-700 rounded-md py-1.5 transition-colors"
|
||||
>
|
||||
{t('areaPane.viewOnGoogleMaps')}
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
{showGoogleMapsLink && (
|
||||
<a
|
||||
href={googleMapsUrl(postcode, j.label || j.slug)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-2 flex items-center justify-center gap-1.5 w-full text-[11px] font-medium text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 bg-white dark:bg-warm-900 border border-warm-200 dark:border-warm-700 rounded-md py-1.5 transition-colors"
|
||||
>
|
||||
<path
|
||||
d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5M7.5 1.5H10.5V4.5M10.5 1.5L5.5 6.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
{t('areaPane.viewOnGoogleMaps')}
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path
|
||||
d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5M7.5 1.5H10.5V4.5M10.5 1.5L5.5 6.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
) : j.minutes != null ? (
|
||||
<div>
|
||||
|
|
@ -297,27 +333,29 @@ export default function JourneyInstructions({
|
|||
{t('areaPane.walk')} · {j.minutes} {t('common.min')}
|
||||
</span>
|
||||
</div>
|
||||
<a
|
||||
href={googleMapsUrl(postcode, j.label || j.slug)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-2 flex items-center justify-center gap-1.5 w-full text-[11px] font-medium text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 bg-white dark:bg-warm-900 border border-warm-200 dark:border-warm-700 rounded-md py-1.5 transition-colors"
|
||||
>
|
||||
{t('areaPane.viewOnGoogleMaps')}
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
{showGoogleMapsLink && (
|
||||
<a
|
||||
href={googleMapsUrl(postcode, j.label || j.slug)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-2 flex items-center justify-center gap-1.5 w-full text-[11px] font-medium text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 bg-white dark:bg-warm-900 border border-warm-200 dark:border-warm-700 rounded-md py-1.5 transition-colors"
|
||||
>
|
||||
<path
|
||||
d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5M7.5 1.5H10.5V4.5M10.5 1.5L5.5 6.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
{t('areaPane.viewOnGoogleMaps')}
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path
|
||||
d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5M7.5 1.5H10.5V4.5M10.5 1.5L5.5 6.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-warm-500 dark:text-warm-400">
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ export interface SearchedLocation {
|
|||
geometry: PostcodeGeometry;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
markerLatitude?: number;
|
||||
markerLongitude?: number;
|
||||
openProperties?: boolean;
|
||||
focusAddress?: string;
|
||||
}
|
||||
|
||||
const ZOOM_FOR_TYPE: Record<string, number> = {
|
||||
|
|
@ -81,6 +85,46 @@ export default function LocationSearch({
|
|||
return;
|
||||
}
|
||||
|
||||
if (result.type === 'address') {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
search.close();
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/postcode/${encodeURIComponent(result.postcode)}`,
|
||||
authHeaders()
|
||||
);
|
||||
if (!res.ok) {
|
||||
setError(t('locationSearch.postcodeNotFound'));
|
||||
return;
|
||||
}
|
||||
const json: {
|
||||
postcode: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
geometry: PostcodeGeometry;
|
||||
} = await res.json();
|
||||
onFlyTo(result.lat, result.lon, 17);
|
||||
onLocationSearched?.({
|
||||
postcode: json.postcode,
|
||||
geometry: json.geometry,
|
||||
latitude: result.lat,
|
||||
longitude: result.lon,
|
||||
markerLatitude: result.lat,
|
||||
markerLongitude: result.lon,
|
||||
openProperties: true,
|
||||
focusAddress: result.address,
|
||||
});
|
||||
search.clear();
|
||||
if (isMobile) setExpanded(false);
|
||||
} catch {
|
||||
setError(t('locationSearch.lookupFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Postcode — fetch geometry
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ interface MapProps {
|
|||
filterRange: [number, number] | null;
|
||||
viewSource: 'drag' | 'eye' | null;
|
||||
onCancelPin: () => void;
|
||||
onResetPreviewScale?: () => void;
|
||||
canResetPreviewScale?: boolean;
|
||||
features: FeatureMeta[];
|
||||
selectedHexagonId: string | null;
|
||||
hoveredHexagonId: string | null;
|
||||
|
|
@ -77,6 +79,49 @@ interface Dimensions {
|
|||
height: number;
|
||||
}
|
||||
|
||||
interface DeckWithPrivateDraw {
|
||||
_drawLayers?: (
|
||||
redrawReason: string,
|
||||
renderOptions?: { viewports?: unknown[]; [key: string]: unknown }
|
||||
) => unknown;
|
||||
__propertyMapNullViewportPatch?: boolean;
|
||||
}
|
||||
|
||||
function patchNullViewportDraw(overlay: MapboxOverlay) {
|
||||
const deck = (overlay as unknown as { _deck?: DeckWithPrivateDraw })._deck;
|
||||
if (!deck || deck.__propertyMapNullViewportPatch || typeof deck._drawLayers !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
const drawLayers = deck._drawLayers.bind(deck);
|
||||
deck._drawLayers = (redrawReason, renderOptions) => {
|
||||
const viewports = renderOptions?.viewports;
|
||||
if (viewports) {
|
||||
// Split-route startup can hand deck.gl a transient null viewport before MapLibre has sized the map.
|
||||
const nonNullViewports = viewports.filter(Boolean);
|
||||
if (nonNullViewports.length === 0) return;
|
||||
if (nonNullViewports.length !== viewports.length) {
|
||||
return drawLayers(redrawReason, { ...renderOptions, viewports: nonNullViewports });
|
||||
}
|
||||
}
|
||||
return drawLayers(redrawReason, renderOptions);
|
||||
};
|
||||
deck.__propertyMapNullViewportPatch = true;
|
||||
}
|
||||
|
||||
class SafeMapboxOverlay extends MapboxOverlay {
|
||||
onAdd(map: unknown) {
|
||||
const element = super.onAdd(map);
|
||||
patchNullViewportDraw(this);
|
||||
return element;
|
||||
}
|
||||
|
||||
setProps(props: Parameters<MapboxOverlay['setProps']>[0]) {
|
||||
super.setProps(props);
|
||||
patchNullViewportDraw(this);
|
||||
}
|
||||
}
|
||||
|
||||
function DeckOverlay({
|
||||
layers,
|
||||
getTooltip,
|
||||
|
|
@ -86,10 +131,13 @@ function DeckOverlay({
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getTooltip: any;
|
||||
}) {
|
||||
const overlay = useControl(() => new MapboxOverlay({ interleaved: true }));
|
||||
const overlay = useControl(() => new SafeMapboxOverlay({ interleaved: true }));
|
||||
|
||||
useEffect(() => {
|
||||
overlay.setProps({ layers: layers.filter(Boolean), getTooltip });
|
||||
overlay.setProps({
|
||||
layers: layers.filter(Boolean),
|
||||
getTooltip,
|
||||
});
|
||||
}, [overlay, layers, getTooltip]);
|
||||
|
||||
return null;
|
||||
|
|
@ -106,6 +154,8 @@ export default memo(function Map({
|
|||
filterRange,
|
||||
viewSource,
|
||||
onCancelPin,
|
||||
onResetPreviewScale,
|
||||
canResetPreviewScale = false,
|
||||
features,
|
||||
selectedHexagonId,
|
||||
hoveredHexagonId,
|
||||
|
|
@ -311,7 +361,7 @@ export default memo(function Map({
|
|||
) : null
|
||||
) : (
|
||||
<>
|
||||
<div className="absolute top-3 left-3 right-3 z-10 flex flex-wrap items-start justify-between gap-2 pointer-events-none">
|
||||
<div className="absolute top-3 left-3 right-3 z-[60] flex flex-wrap items-start justify-between gap-2 pointer-events-none">
|
||||
<LocationSearch
|
||||
onFlyTo={handleFlyTo}
|
||||
onLocationSearched={onLocationSearched}
|
||||
|
|
@ -330,6 +380,8 @@ export default memo(function Map({
|
|||
range={colorRange}
|
||||
showCancel={viewSource === 'eye'}
|
||||
onCancel={onCancelPin}
|
||||
onResetScale={viewSource === 'eye' ? onResetPreviewScale : undefined}
|
||||
resetScaleDisabled={!canResetPreviewScale}
|
||||
mode="feature"
|
||||
theme={theme}
|
||||
suffix=" min"
|
||||
|
|
@ -344,6 +396,8 @@ export default memo(function Map({
|
|||
range={colorRange}
|
||||
showCancel={viewSource === 'eye'}
|
||||
onCancel={onCancelPin}
|
||||
onResetScale={viewSource === 'eye' ? onResetPreviewScale : undefined}
|
||||
resetScaleDisabled={!canResetPreviewScale}
|
||||
mode="feature"
|
||||
enumValues={
|
||||
colorFeatureMeta.type === 'enum' ? colorFeatureMeta.values : undefined
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { lazy, Suspense, useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cellToLatLng } from 'h3-js';
|
||||
import type {
|
||||
|
|
@ -11,15 +11,8 @@ import type {
|
|||
} from '../../types';
|
||||
import type { SearchedLocation } from './LocationSearch';
|
||||
import type { Page } from '../ui/Header';
|
||||
import Map from './Map';
|
||||
import Filters from './Filters';
|
||||
import POIPane from './POIPane';
|
||||
import { PropertiesPane } from './PropertiesPane';
|
||||
import AreaPane from './AreaPane';
|
||||
import MobileDrawer from './MobileDrawer';
|
||||
import MobileBottomSheet from './MobileBottomSheet';
|
||||
import MapLegend from './MapLegend';
|
||||
import { MapPageSelectionPane } from './MapPageSelectionPane';
|
||||
import { useMapData } from '../../hooks/useMapData';
|
||||
import { usePOIData } from '../../hooks/usePOIData';
|
||||
import { useFilters } from '../../hooks/useFilters';
|
||||
|
|
@ -30,7 +23,6 @@ import { useAiFilters } from '../../hooks/useAiFilters';
|
|||
import { useUrlSync } from '../../hooks/useUrlSync';
|
||||
import { useTutorial } from '../../hooks/useTutorial';
|
||||
import { getTutorialStyles } from '../../lib/tutorial-styles';
|
||||
import Joyride from 'react-joyride';
|
||||
import {
|
||||
useTravelTime,
|
||||
useTranslatedModes,
|
||||
|
|
@ -44,11 +36,40 @@ import { trackEvent } from '../../lib/analytics';
|
|||
import { INITIAL_VIEW_STATE } from '../../lib/consts';
|
||||
import { getSchoolBackendFeatureName } from '../../lib/school-filter';
|
||||
import { useLicense } from '../../hooks/useLicense';
|
||||
import UpgradeModal from '../ui/UpgradeModal';
|
||||
import { SpinnerIcon } from '../ui/icons/SpinnerIcon';
|
||||
import { MapPinIcon } from '../ui/icons/MapPinIcon';
|
||||
import { BookmarkIcon } from '../ui/icons/BookmarkIcon';
|
||||
|
||||
const Map = lazy(() => import('./Map'));
|
||||
const Filters = lazy(() => import('./Filters'));
|
||||
const POIPane = lazy(() => import('./POIPane'));
|
||||
const AreaPane = lazy(() => import('./AreaPane'));
|
||||
const PropertiesPane = lazy(() =>
|
||||
import('./PropertiesPane').then((module) => ({ default: module.PropertiesPane }))
|
||||
);
|
||||
const MobileDrawer = lazy(() => import('./MobileDrawer'));
|
||||
const MapPageSelectionPane = lazy(() =>
|
||||
import('./MapPageSelectionPane').then((module) => ({ default: module.MapPageSelectionPane }))
|
||||
);
|
||||
const UpgradeModal = lazy(() => import('../ui/UpgradeModal'));
|
||||
const Joyride = lazy(() => import('react-joyride'));
|
||||
|
||||
function MapFallback() {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-warm-100 dark:bg-navy-950">
|
||||
<SpinnerIcon className="h-8 w-8 animate-spin text-teal-600 dark:text-teal-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PaneFallback() {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-white dark:bg-navy-950">
|
||||
<SpinnerIcon className="h-6 w-6 animate-spin text-teal-600 dark:text-teal-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ExportState {
|
||||
onExport: () => void;
|
||||
exporting: boolean;
|
||||
|
|
@ -193,6 +214,7 @@ export default function MapPage({
|
|||
features,
|
||||
viewFeature,
|
||||
activeFeature,
|
||||
pinnedFeature,
|
||||
travelTimeEntries: entries,
|
||||
shareCode,
|
||||
});
|
||||
|
|
@ -335,8 +357,19 @@ export default function MapPage({
|
|||
const handleLocationSearchResult = useCallback(
|
||||
(result: SearchedLocation | null) => {
|
||||
if (result) {
|
||||
setCurrentLocation(null);
|
||||
handleLocationSearch(result.postcode, result.geometry, result.latitude, result.longitude);
|
||||
if (result.markerLatitude != null && result.markerLongitude != null) {
|
||||
setCurrentLocation({ lat: result.markerLatitude, lng: result.markerLongitude });
|
||||
} else {
|
||||
setCurrentLocation(null);
|
||||
}
|
||||
handleLocationSearch(
|
||||
result.postcode,
|
||||
result.geometry,
|
||||
result.latitude,
|
||||
result.longitude,
|
||||
result.openProperties,
|
||||
result.focusAddress
|
||||
);
|
||||
if (isMobile) setMobileDrawerOpen(true);
|
||||
} else {
|
||||
setCurrentLocation(null);
|
||||
|
|
@ -604,121 +637,134 @@ export default function MapPage({
|
|||
if (screenshotMode) {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<Map
|
||||
data={mapData.data}
|
||||
postcodeData={mapData.postcodeData}
|
||||
usePostcodeView={mapData.usePostcodeView}
|
||||
pois={[]}
|
||||
onViewChange={mapData.handleViewChange}
|
||||
viewFeature={mapViewFeature}
|
||||
colorRange={mapData.colorRange}
|
||||
filterRange={filterRange}
|
||||
viewSource={viewSource}
|
||||
onCancelPin={() => {}}
|
||||
features={features}
|
||||
selectedHexagonId={null}
|
||||
hoveredHexagonId={null}
|
||||
onHexagonClick={() => {}}
|
||||
onHexagonHover={() => {}}
|
||||
initialViewState={initialViewState}
|
||||
theme={theme}
|
||||
screenshotMode
|
||||
ogMode={ogMode}
|
||||
bounds={mapData.bounds}
|
||||
travelTimeEntries={entries}
|
||||
/>
|
||||
<Suspense fallback={<MapFallback />}>
|
||||
<Map
|
||||
data={mapData.data}
|
||||
postcodeData={mapData.postcodeData}
|
||||
usePostcodeView={mapData.usePostcodeView}
|
||||
pois={[]}
|
||||
onViewChange={mapData.handleViewChange}
|
||||
viewFeature={mapViewFeature}
|
||||
colorRange={mapData.colorRange}
|
||||
filterRange={filterRange}
|
||||
viewSource={viewSource}
|
||||
onCancelPin={() => {}}
|
||||
onResetPreviewScale={mapData.handleResetPreviewScale}
|
||||
canResetPreviewScale={mapData.canResetPreviewScale}
|
||||
features={features}
|
||||
selectedHexagonId={null}
|
||||
hoveredHexagonId={null}
|
||||
onHexagonClick={() => {}}
|
||||
onHexagonHover={() => {}}
|
||||
initialViewState={initialViewState}
|
||||
theme={theme}
|
||||
screenshotMode
|
||||
ogMode={ogMode}
|
||||
bounds={mapData.bounds}
|
||||
travelTimeEntries={entries}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderAreaPane = () => (
|
||||
<AreaPane
|
||||
stats={areaStats}
|
||||
globalFeatures={features}
|
||||
loading={loadingAreaStats}
|
||||
hexagonId={selectedHexagon?.id || null}
|
||||
isPostcode={selectedHexagon?.type === 'postcode'}
|
||||
postcodeData={
|
||||
selectedHexagon?.type === 'postcode'
|
||||
? mapData.postcodeData.find((f) => f.properties.postcode === selectedHexagon?.id) || null
|
||||
: null
|
||||
}
|
||||
onViewProperties={handleViewPropertiesFromArea}
|
||||
onClearFilters={hasActiveFilters ? handleClearAll : undefined}
|
||||
hexagonLocation={hexagonLocation}
|
||||
filters={filters}
|
||||
unfilteredCount={unfilteredAreaCount}
|
||||
travelTimeEntries={activeEntries}
|
||||
isGroupExpanded={isAreaGroupExpanded}
|
||||
onToggleGroup={toggleAreaGroup}
|
||||
/>
|
||||
<Suspense fallback={<PaneFallback />}>
|
||||
<AreaPane
|
||||
stats={areaStats}
|
||||
globalFeatures={features}
|
||||
loading={loadingAreaStats}
|
||||
hexagonId={selectedHexagon?.id || null}
|
||||
isPostcode={selectedHexagon?.type === 'postcode'}
|
||||
postcodeData={
|
||||
selectedHexagon?.type === 'postcode'
|
||||
? mapData.postcodeData.find((f) => f.properties.postcode === selectedHexagon?.id) ||
|
||||
null
|
||||
: null
|
||||
}
|
||||
onViewProperties={handleViewPropertiesFromArea}
|
||||
onClearFilters={hasActiveFilters ? handleClearAll : undefined}
|
||||
hexagonLocation={hexagonLocation}
|
||||
filters={filters}
|
||||
unfilteredCount={unfilteredAreaCount}
|
||||
travelTimeEntries={activeEntries}
|
||||
isGroupExpanded={isAreaGroupExpanded}
|
||||
onToggleGroup={toggleAreaGroup}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
const renderPropertiesPane = () => (
|
||||
<PropertiesPane
|
||||
properties={properties}
|
||||
total={propertiesTotal}
|
||||
loading={loadingProperties}
|
||||
hexagonId={selectedHexagon?.id || null}
|
||||
onLoadMore={handleLoadMoreProperties}
|
||||
onSaveProperty={onSaveProperty ? handleSavePropertyWithToast : undefined}
|
||||
onUnsaveProperty={onUnsaveProperty}
|
||||
isPropertySaved={isPropertySaved}
|
||||
getSavedPropertyId={getSavedPropertyId}
|
||||
/>
|
||||
<Suspense fallback={<PaneFallback />}>
|
||||
<PropertiesPane
|
||||
properties={properties}
|
||||
total={propertiesTotal}
|
||||
loading={loadingProperties}
|
||||
hexagonId={selectedHexagon?.id || null}
|
||||
onLoadMore={handleLoadMoreProperties}
|
||||
onSaveProperty={onSaveProperty ? handleSavePropertyWithToast : undefined}
|
||||
onUnsaveProperty={onUnsaveProperty}
|
||||
isPropertySaved={isPropertySaved}
|
||||
getSavedPropertyId={getSavedPropertyId}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
const renderPOIPane = () => (
|
||||
<POIPane
|
||||
groups={poiCategoryGroups}
|
||||
selectedCategories={selectedPOICategories}
|
||||
onCategoriesChange={setSelectedPOICategories}
|
||||
poiCount={pois.length}
|
||||
onClose={() => setPoiPaneOpen(false)}
|
||||
/>
|
||||
<Suspense fallback={<PaneFallback />}>
|
||||
<POIPane
|
||||
groups={poiCategoryGroups}
|
||||
selectedCategories={selectedPOICategories}
|
||||
onCategoriesChange={setSelectedPOICategories}
|
||||
poiCount={pois.length}
|
||||
onClose={() => setPoiPaneOpen(false)}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
const renderFilters = (options?: { destinationDropdownPortal?: boolean }) => (
|
||||
<Filters
|
||||
features={features}
|
||||
filters={filters}
|
||||
activeFeature={activeFeature}
|
||||
dragValue={dragValue}
|
||||
enabledFeatures={enabledFeatures}
|
||||
onAddFilter={handleAddFilter}
|
||||
onRemoveFilter={handleRemoveFilter}
|
||||
onFilterChange={handleFilterChange}
|
||||
onDragStart={handleDragStart}
|
||||
onDragChange={handleDragChange}
|
||||
onDragEnd={handleDragEnd}
|
||||
pinnedFeature={pinnedFeature}
|
||||
onTogglePin={handleTogglePin}
|
||||
openInfoFeature={pendingInfoFeature}
|
||||
onClearOpenInfoFeature={onClearPendingInfoFeature}
|
||||
travelTimeEntries={entries}
|
||||
onTravelTimeAddEntry={handleAddEntry}
|
||||
onTravelTimeRemoveEntry={handleTravelTimeRemoveEntry}
|
||||
onTravelTimeSetDestination={handleTravelTimeSetDestination}
|
||||
onTravelTimeRangeChange={handleTimeRangeChange}
|
||||
onTravelTimeDragEnd={handleTravelTimeDragEnd}
|
||||
onTravelTimeToggleBest={handleToggleBest}
|
||||
aiFilterLoading={aiFilterLoading}
|
||||
aiFilterError={aiFilterError}
|
||||
aiFilterErrorType={aiFilterErrorType}
|
||||
aiFilterNotes={aiFilterNotes}
|
||||
aiFilterSummary={aiFilterSummary}
|
||||
onAiFilterSubmit={handleAiFilterSubmit}
|
||||
isLoggedIn={!!user}
|
||||
onLoginRequired={onRegisterClick ?? (() => {})}
|
||||
isLicensed={user?.subscription === 'licensed'}
|
||||
onUpgradeClick={() => onNavigateTo('pricing')}
|
||||
onResetTutorial={tutorial.resetTutorial}
|
||||
filterImpacts={filterCounts.impacts}
|
||||
onClearAll={handleClearAll}
|
||||
onSaveSearch={onSaveSearch}
|
||||
savingSearch={savingSearch}
|
||||
destinationDropdownPortal={options?.destinationDropdownPortal}
|
||||
/>
|
||||
<Suspense fallback={<PaneFallback />}>
|
||||
<Filters
|
||||
features={features}
|
||||
filters={filters}
|
||||
activeFeature={activeFeature}
|
||||
dragValue={dragValue}
|
||||
enabledFeatures={enabledFeatures}
|
||||
onAddFilter={handleAddFilter}
|
||||
onRemoveFilter={handleRemoveFilter}
|
||||
onFilterChange={handleFilterChange}
|
||||
onDragStart={handleDragStart}
|
||||
onDragChange={handleDragChange}
|
||||
onDragEnd={handleDragEnd}
|
||||
pinnedFeature={pinnedFeature}
|
||||
onTogglePin={handleTogglePin}
|
||||
openInfoFeature={pendingInfoFeature}
|
||||
onClearOpenInfoFeature={onClearPendingInfoFeature}
|
||||
travelTimeEntries={entries}
|
||||
onTravelTimeAddEntry={handleAddEntry}
|
||||
onTravelTimeRemoveEntry={handleTravelTimeRemoveEntry}
|
||||
onTravelTimeSetDestination={handleTravelTimeSetDestination}
|
||||
onTravelTimeRangeChange={handleTimeRangeChange}
|
||||
onTravelTimeDragEnd={handleTravelTimeDragEnd}
|
||||
onTravelTimeToggleBest={handleToggleBest}
|
||||
aiFilterLoading={aiFilterLoading}
|
||||
aiFilterError={aiFilterError}
|
||||
aiFilterErrorType={aiFilterErrorType}
|
||||
aiFilterNotes={aiFilterNotes}
|
||||
aiFilterSummary={aiFilterSummary}
|
||||
onAiFilterSubmit={handleAiFilterSubmit}
|
||||
isLoggedIn={!!user}
|
||||
onLoginRequired={onRegisterClick ?? (() => {})}
|
||||
isLicensed={user?.subscription === 'licensed'}
|
||||
onUpgradeClick={() => onNavigateTo('pricing')}
|
||||
onResetTutorial={tutorial.resetTutorial}
|
||||
filterImpacts={filterCounts.impacts}
|
||||
onClearAll={handleClearAll}
|
||||
onSaveSearch={onSaveSearch}
|
||||
savingSearch={savingSearch}
|
||||
destinationDropdownPortal={options?.destinationDropdownPortal}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
const renderMobileLegend = () => {
|
||||
|
|
@ -734,6 +780,8 @@ export default function MapPage({
|
|||
range={mapData.colorRange}
|
||||
showCancel={viewSource === 'eye'}
|
||||
onCancel={handleCancelPin}
|
||||
onResetScale={viewSource === 'eye' ? mapData.handleResetPreviewScale : undefined}
|
||||
resetScaleDisabled={!mapData.canResetPreviewScale}
|
||||
mode="feature"
|
||||
theme={theme}
|
||||
inline
|
||||
|
|
@ -753,6 +801,8 @@ export default function MapPage({
|
|||
range={mapData.colorRange}
|
||||
showCancel={viewSource === 'eye'}
|
||||
onCancel={handleCancelPin}
|
||||
onResetScale={viewSource === 'eye' ? mapData.handleResetPreviewScale : undefined}
|
||||
resetScaleDisabled={!mapData.canResetPreviewScale}
|
||||
mode="feature"
|
||||
enumValues={mobileLegendMeta.type === 'enum' ? mobileLegendMeta.values : undefined}
|
||||
featureName={mobileLegendMeta.name}
|
||||
|
|
@ -794,34 +844,38 @@ export default function MapPage({
|
|||
)}
|
||||
|
||||
<div className="absolute inset-0">
|
||||
<Map
|
||||
data={mapData.data}
|
||||
postcodeData={mapData.postcodeData}
|
||||
usePostcodeView={mapData.usePostcodeView}
|
||||
pois={pois}
|
||||
onViewChange={mapData.handleViewChange}
|
||||
viewFeature={mapViewFeature}
|
||||
colorRange={mapData.colorRange}
|
||||
filterRange={filterRange}
|
||||
viewSource={viewSource}
|
||||
onCancelPin={handleCancelPin}
|
||||
features={features}
|
||||
selectedHexagonId={selectedHexagon?.id || null}
|
||||
hoveredHexagonId={hoveredHexagon}
|
||||
onHexagonClick={handleMobileHexagonClick}
|
||||
onHexagonHover={handleHexagonHover}
|
||||
initialViewState={initialViewState}
|
||||
flyToRef={mapFlyToRef}
|
||||
theme={theme}
|
||||
filters={filters}
|
||||
selectedPostcodeGeometry={selectedPostcodeGeometry}
|
||||
onLocationSearched={handleLocationSearchResult}
|
||||
onCurrentLocationFound={handleCurrentLocationFound}
|
||||
currentLocation={currentLocation}
|
||||
bounds={mapData.bounds}
|
||||
hideLegend
|
||||
travelTimeEntries={entries}
|
||||
/>
|
||||
<Suspense fallback={<MapFallback />}>
|
||||
<Map
|
||||
data={mapData.data}
|
||||
postcodeData={mapData.postcodeData}
|
||||
usePostcodeView={mapData.usePostcodeView}
|
||||
pois={pois}
|
||||
onViewChange={mapData.handleViewChange}
|
||||
viewFeature={mapViewFeature}
|
||||
colorRange={mapData.colorRange}
|
||||
filterRange={filterRange}
|
||||
viewSource={viewSource}
|
||||
onCancelPin={handleCancelPin}
|
||||
onResetPreviewScale={mapData.handleResetPreviewScale}
|
||||
canResetPreviewScale={mapData.canResetPreviewScale}
|
||||
features={features}
|
||||
selectedHexagonId={selectedHexagon?.id || null}
|
||||
hoveredHexagonId={hoveredHexagon}
|
||||
onHexagonClick={handleMobileHexagonClick}
|
||||
onHexagonHover={handleHexagonHover}
|
||||
initialViewState={initialViewState}
|
||||
flyToRef={mapFlyToRef}
|
||||
theme={theme}
|
||||
filters={filters}
|
||||
selectedPostcodeGeometry={selectedPostcodeGeometry}
|
||||
onLocationSearched={handleLocationSearchResult}
|
||||
onCurrentLocationFound={handleCurrentLocationFound}
|
||||
currentLocation={currentLocation}
|
||||
bounds={mapData.bounds}
|
||||
hideLegend
|
||||
travelTimeEntries={entries}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
{mapData.loading && (
|
||||
|
|
@ -849,40 +903,41 @@ export default function MapPage({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<MobileBottomSheet
|
||||
activeCount={Object.keys(filters).length + entries.length}
|
||||
legend={renderMobileLegend()}
|
||||
>
|
||||
<MobileBottomSheet legend={renderMobileLegend()}>
|
||||
{renderFilters({ destinationDropdownPortal: false })}
|
||||
</MobileBottomSheet>
|
||||
|
||||
{mobileDrawerOpen && selectedHexagon && (
|
||||
<MobileDrawer
|
||||
onClose={() => setMobileDrawerOpen(false)}
|
||||
renderArea={renderAreaPane}
|
||||
renderProperties={renderPropertiesPane}
|
||||
tab={rightPaneTab}
|
||||
onTabChange={(t) => {
|
||||
if (t === 'properties') {
|
||||
handlePropertiesTabClick();
|
||||
} else {
|
||||
setRightPaneTab(t);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Suspense fallback={<PaneFallback />}>
|
||||
<MobileDrawer
|
||||
onClose={() => setMobileDrawerOpen(false)}
|
||||
renderArea={renderAreaPane}
|
||||
renderProperties={renderPropertiesPane}
|
||||
tab={rightPaneTab}
|
||||
onTabChange={(t) => {
|
||||
if (t === 'properties') {
|
||||
handlePropertiesTabClick();
|
||||
} else {
|
||||
setRightPaneTab(t);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{bookmarkToast}
|
||||
|
||||
{mapData.licenseRequired && (
|
||||
<UpgradeModal
|
||||
isLoggedIn={!!user}
|
||||
onLoginClick={onLoginClick ?? (() => {})}
|
||||
onRegisterClick={onRegisterClick ?? (() => {})}
|
||||
onStartCheckout={() => license.startCheckout()}
|
||||
onZoomToFreeZone={handleZoomToFreeZone}
|
||||
isShareReturn={!!shareReturnViewRef.current}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<UpgradeModal
|
||||
isLoggedIn={!!user}
|
||||
onLoginClick={onLoginClick ?? (() => {})}
|
||||
onRegisterClick={onRegisterClick ?? (() => {})}
|
||||
onStartCheckout={() => license.startCheckout()}
|
||||
onZoomToFreeZone={handleZoomToFreeZone}
|
||||
isShareReturn={!!shareReturnViewRef.current}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -901,17 +956,21 @@ export default function MapPage({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<Joyride
|
||||
steps={tutorial.steps}
|
||||
run={tutorial.run}
|
||||
continuous
|
||||
showProgress
|
||||
showSkipButton
|
||||
callback={tutorial.handleCallback}
|
||||
styles={getTutorialStyles(theme)}
|
||||
disableScrolling
|
||||
locale={{ last: 'Finish' }}
|
||||
/>
|
||||
{tutorial.run && (
|
||||
<Suspense fallback={null}>
|
||||
<Joyride
|
||||
steps={tutorial.steps}
|
||||
run={tutorial.run}
|
||||
continuous
|
||||
showProgress
|
||||
showSkipButton
|
||||
callback={tutorial.handleCallback}
|
||||
styles={getTutorialStyles(theme)}
|
||||
disableScrolling
|
||||
locale={{ last: 'Finish' }}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
<div
|
||||
data-tutorial="filters"
|
||||
|
|
@ -932,35 +991,39 @@ export default function MapPage({
|
|||
</div>
|
||||
|
||||
<div data-tutorial="map" className="flex-1 relative">
|
||||
<Map
|
||||
data={mapData.data}
|
||||
postcodeData={mapData.postcodeData}
|
||||
usePostcodeView={mapData.usePostcodeView}
|
||||
pois={pois}
|
||||
onViewChange={mapData.handleViewChange}
|
||||
viewFeature={mapViewFeature}
|
||||
colorRange={mapData.colorRange}
|
||||
filterRange={filterRange}
|
||||
viewSource={viewSource}
|
||||
onCancelPin={handleCancelPin}
|
||||
features={features}
|
||||
selectedHexagonId={selectedHexagon?.id || null}
|
||||
hoveredHexagonId={hoveredHexagon}
|
||||
onHexagonClick={handleHexagonClick}
|
||||
onHexagonHover={handleHexagonHover}
|
||||
initialViewState={initialViewState}
|
||||
flyToRef={mapFlyToRef}
|
||||
theme={theme}
|
||||
filters={filters}
|
||||
selectedPostcodeGeometry={selectedPostcodeGeometry}
|
||||
onLocationSearched={handleLocationSearchResult}
|
||||
onCurrentLocationFound={handleCurrentLocationFound}
|
||||
currentLocation={currentLocation}
|
||||
bounds={mapData.bounds}
|
||||
travelTimeEntries={entries}
|
||||
densityLabel={densityLabel}
|
||||
totalCount={hasActiveFilters ? filterCounts.total : undefined}
|
||||
/>
|
||||
<Suspense fallback={<MapFallback />}>
|
||||
<Map
|
||||
data={mapData.data}
|
||||
postcodeData={mapData.postcodeData}
|
||||
usePostcodeView={mapData.usePostcodeView}
|
||||
pois={pois}
|
||||
onViewChange={mapData.handleViewChange}
|
||||
viewFeature={mapViewFeature}
|
||||
colorRange={mapData.colorRange}
|
||||
filterRange={filterRange}
|
||||
viewSource={viewSource}
|
||||
onCancelPin={handleCancelPin}
|
||||
onResetPreviewScale={mapData.handleResetPreviewScale}
|
||||
canResetPreviewScale={mapData.canResetPreviewScale}
|
||||
features={features}
|
||||
selectedHexagonId={selectedHexagon?.id || null}
|
||||
hoveredHexagonId={hoveredHexagon}
|
||||
onHexagonClick={handleHexagonClick}
|
||||
onHexagonHover={handleHexagonHover}
|
||||
initialViewState={initialViewState}
|
||||
flyToRef={mapFlyToRef}
|
||||
theme={theme}
|
||||
filters={filters}
|
||||
selectedPostcodeGeometry={selectedPostcodeGeometry}
|
||||
onLocationSearched={handleLocationSearchResult}
|
||||
onCurrentLocationFound={handleCurrentLocationFound}
|
||||
currentLocation={currentLocation}
|
||||
bounds={mapData.bounds}
|
||||
travelTimeEntries={entries}
|
||||
densityLabel={densityLabel}
|
||||
totalCount={hasActiveFilters ? filterCounts.total : undefined}
|
||||
/>
|
||||
</Suspense>
|
||||
{mapData.loading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div className="flex items-center gap-2 bg-white/90 dark:bg-warm-800/90 px-4 py-2.5 rounded-lg shadow-lg pointer-events-auto">
|
||||
|
|
@ -989,29 +1052,33 @@ export default function MapPage({
|
|||
</div>
|
||||
|
||||
{selectedHexagon && (
|
||||
<MapPageSelectionPane
|
||||
width={rightPaneWidth}
|
||||
resizeHandlers={rightPaneHandlers}
|
||||
tab={rightPaneTab}
|
||||
onAreaTabClick={() => setRightPaneTab('area')}
|
||||
onPropertiesTabClick={handlePropertiesTabClick}
|
||||
onClose={handleCloseSelection}
|
||||
renderAreaPane={renderAreaPane}
|
||||
renderPropertiesPane={renderPropertiesPane}
|
||||
/>
|
||||
<Suspense fallback={<PaneFallback />}>
|
||||
<MapPageSelectionPane
|
||||
width={rightPaneWidth}
|
||||
resizeHandlers={rightPaneHandlers}
|
||||
tab={rightPaneTab}
|
||||
onAreaTabClick={() => setRightPaneTab('area')}
|
||||
onPropertiesTabClick={handlePropertiesTabClick}
|
||||
onClose={handleCloseSelection}
|
||||
renderAreaPane={renderAreaPane}
|
||||
renderPropertiesPane={renderPropertiesPane}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{bookmarkToast}
|
||||
|
||||
{mapData.licenseRequired && (
|
||||
<UpgradeModal
|
||||
isLoggedIn={!!user}
|
||||
onLoginClick={onLoginClick ?? (() => {})}
|
||||
onRegisterClick={onRegisterClick ?? (() => {})}
|
||||
onStartCheckout={() => license.startCheckout()}
|
||||
onZoomToFreeZone={handleZoomToFreeZone}
|
||||
isShareReturn={!!shareReturnViewRef.current}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<UpgradeModal
|
||||
isLoggedIn={!!user}
|
||||
onLoginClick={onLoginClick ?? (() => {})}
|
||||
onRegisterClick={onRegisterClick ?? (() => {})}
|
||||
onStartCheckout={() => license.startCheckout()}
|
||||
onZoomToFreeZone={handleZoomToFreeZone}
|
||||
isShareReturn={!!shareReturnViewRef.current}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface VisualViewportState {
|
||||
height: number;
|
||||
|
|
@ -8,7 +7,6 @@ interface VisualViewportState {
|
|||
}
|
||||
|
||||
interface MobileBottomSheetProps {
|
||||
activeCount: number;
|
||||
children: ReactNode;
|
||||
legend?: ReactNode;
|
||||
}
|
||||
|
|
@ -57,11 +55,9 @@ function clamp(value: number, min: number, max: number): number {
|
|||
}
|
||||
|
||||
export default function MobileBottomSheet({
|
||||
activeCount,
|
||||
children,
|
||||
legend,
|
||||
}: MobileBottomSheetProps) {
|
||||
const { t } = useTranslation();
|
||||
const viewport = useVisualViewportState();
|
||||
const sheetRef = useRef<HTMLDivElement>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -133,8 +129,6 @@ export default function MobileBottomSheet({
|
|||
return () => sheet.removeEventListener('focusin', handleFocusIn);
|
||||
}, [heightBounds.initial, heightBounds.max, viewport.height]);
|
||||
|
||||
const sheetTitle = activeCount === 0 ? t('filters.chooseFilters') : t('filters.activeFilters');
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sheetRef}
|
||||
|
|
@ -148,29 +142,16 @@ export default function MobileBottomSheet({
|
|||
? undefined
|
||||
: 'height 140ms ease, bottom 180ms ease',
|
||||
}}
|
||||
aria-label={sheetTitle}
|
||||
>
|
||||
<div
|
||||
className="shrink-0 touch-none px-4 pt-2 pb-1"
|
||||
className="shrink-0 touch-none px-4 py-2"
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerCancel={handlePointerUp}
|
||||
>
|
||||
<div className="w-full flex flex-col items-center gap-2" role="presentation">
|
||||
<div className="w-full flex items-center justify-center" role="presentation">
|
||||
<span className="h-1.5 w-12 rounded-full bg-warm-300 dark:bg-navy-600" />
|
||||
<span className="w-full flex items-center justify-between">
|
||||
<span className="flex items-center gap-2 min-w-0">
|
||||
<span className="text-sm font-semibold text-navy-950 dark:text-warm-100 truncate">
|
||||
{sheetTitle}
|
||||
</span>
|
||||
{activeCount > 0 && (
|
||||
<span className="text-xs font-medium px-1.5 py-0.5 rounded-full bg-teal-50 dark:bg-teal-900/30 text-teal-600 dark:text-teal-400">
|
||||
{activeCount}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ interface PriceHistoryChartProps {
|
|||
points: PricePoint[];
|
||||
}
|
||||
|
||||
const PADDING = { top: 8, right: 8, bottom: 20, left: 42 };
|
||||
const PADDING = { top: 8, right: 24, bottom: 20, left: 42 };
|
||||
const HEIGHT = 120;
|
||||
const priceFmt = { prefix: '£' };
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ export default function Header({
|
|||
}`;
|
||||
|
||||
return (
|
||||
<header className="h-12 bg-navy-900 text-white flex items-center px-4 shrink-0">
|
||||
<header className="relative z-50 h-12 bg-navy-900 text-white flex items-center px-4 shrink-0">
|
||||
{/* Left: Logo + nav */}
|
||||
<div className="flex items-center gap-4">
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -156,15 +156,16 @@ export default function MobileMenu({
|
|||
</button>
|
||||
|
||||
{/* Language selector */}
|
||||
<div className="flex gap-1 px-4">
|
||||
<div className="flex max-w-full gap-1 overflow-x-auto overflow-y-hidden px-4 pb-1 scrollbar-hide">
|
||||
{SUPPORTED_LANGUAGES.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
aria-label={lang.label}
|
||||
onClick={() => {
|
||||
localStorage.setItem('language', lang.code);
|
||||
void changeAppLanguage(lang.code);
|
||||
}}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 px-2 py-2 rounded text-sm ${
|
||||
className={`flex-none min-w-[2.75rem] flex items-center justify-center gap-1.5 px-2 py-2 rounded text-sm ${
|
||||
i18n.language === lang.code
|
||||
? 'bg-navy-700 text-white font-medium'
|
||||
: 'text-warm-400 hover:bg-navy-800 hover:text-white'
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { SearchResult } from '../../hooks/useLocationSearch';
|
|||
import { useDropdownPosition } from '../../hooks/useDropdownPosition';
|
||||
import { SearchIcon } from './icons/SearchIcon';
|
||||
import { MapPinIcon } from './icons/MapPinIcon';
|
||||
import { HouseIcon } from './icons/HouseIcon';
|
||||
|
||||
interface SearchHook {
|
||||
query: string;
|
||||
|
|
@ -66,7 +67,11 @@ export function PlaceSearchInput({
|
|||
{search.results.map((result, idx) => (
|
||||
<button
|
||||
key={
|
||||
result.type === 'postcode' ? `pc-${result.label}` : `pl-${result.name}-${result.lat}`
|
||||
result.type === 'postcode'
|
||||
? `pc-${result.label}`
|
||||
: result.type === 'address'
|
||||
? `addr-${result.postcode}-${result.address}-${result.lat}`
|
||||
: `pl-${result.name}-${result.lat}`
|
||||
}
|
||||
type="button"
|
||||
className={`w-full text-left flex items-center cursor-pointer ${
|
||||
|
|
@ -87,6 +92,16 @@ export function PlaceSearchInput({
|
|||
<SearchIcon className={`${iconSize} text-warm-400 dark:text-warm-500 shrink-0`} />
|
||||
<span className="text-warm-700 dark:text-warm-200">{result.label}</span>
|
||||
</>
|
||||
) : result.type === 'address' ? (
|
||||
<>
|
||||
<HouseIcon className={`${iconSize} text-warm-400 dark:text-warm-500 shrink-0`} />
|
||||
<span className="min-w-0 text-warm-700 dark:text-warm-200">
|
||||
<span className="block truncate">{result.address}</span>
|
||||
<span className="block truncate text-warm-400 dark:text-warm-500">
|
||||
{result.postcode}
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MapPinIcon className={`${iconSize} text-warm-400 dark:text-warm-500 shrink-0`} />
|
||||
|
|
|
|||
|
|
@ -84,7 +84,9 @@ export default function UpgradeModal({
|
|||
{priceLabel}
|
||||
</span>
|
||||
{!isFree && (
|
||||
<span className="text-warm-500 dark:text-warm-400 text-lg">{t('upgrade.once')}</span>
|
||||
<span className="text-warm-500 dark:text-warm-400 text-lg">
|
||||
{t('pricingPage.lifetime')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-center text-sm text-warm-500 dark:text-warm-400 mb-6">
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ export function useHexagonSelection({
|
|||
);
|
||||
|
||||
const fetchPostcodeProperties = useCallback(
|
||||
async (postcode: string, offset = 0) => {
|
||||
async (postcode: string, offset = 0, focusAddress?: string) => {
|
||||
setLoadingProperties(true);
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
|
|
@ -186,6 +186,9 @@ export function useHexagonSelection({
|
|||
limit: '100',
|
||||
offset: offset.toString(),
|
||||
});
|
||||
if (focusAddress && offset === 0) {
|
||||
params.set('focus_address', focusAddress);
|
||||
}
|
||||
|
||||
const filterStr = buildFilterString(filters, features);
|
||||
if (filterStr) params.append('filters', filterStr);
|
||||
|
|
@ -464,13 +467,20 @@ export function useHexagonSelection({
|
|||
]);
|
||||
|
||||
const handleLocationSearch = useCallback(
|
||||
(postcode: string, geometry: PostcodeGeometry, lat?: number, lng?: number) => {
|
||||
trackEvent('Postcode Search');
|
||||
(
|
||||
postcode: string,
|
||||
geometry: PostcodeGeometry,
|
||||
lat?: number,
|
||||
lng?: number,
|
||||
openProperties = false,
|
||||
focusAddress?: string
|
||||
) => {
|
||||
trackEvent(openProperties ? 'Address Search' : 'Postcode Search');
|
||||
setProperties([]);
|
||||
setPropertiesTotal(0);
|
||||
setPropertiesOffset(0);
|
||||
setUnfilteredAreaCount(null);
|
||||
setRightPaneTab('area');
|
||||
setRightPaneTab(openProperties ? 'properties' : 'area');
|
||||
setLoadingAreaStats(true);
|
||||
|
||||
// First try the postcode; if it has no properties, fall back to hexagons
|
||||
|
|
@ -482,6 +492,9 @@ export function useHexagonSelection({
|
|||
setSelectedPostcodeGeometry(geometry);
|
||||
setAreaStats(stats);
|
||||
refreshUnfilteredAreaCount(selection, stats.count);
|
||||
if (openProperties) {
|
||||
fetchPostcodeProperties(postcode, 0, focusAddress);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -493,6 +506,7 @@ export function useHexagonSelection({
|
|||
setSelectedPostcodeGeometry(geometry);
|
||||
setAreaStats(stats);
|
||||
refreshUnfilteredAreaCount(selection, stats.count);
|
||||
setRightPaneTab('area');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -507,6 +521,7 @@ export function useHexagonSelection({
|
|||
setSelectedPostcodeGeometry(null);
|
||||
setAreaStats(hexStats);
|
||||
refreshUnfilteredAreaCount(selection, hexStats.count);
|
||||
setRightPaneTab('area');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -519,11 +534,18 @@ export function useHexagonSelection({
|
|||
setSelectedPostcodeGeometry(null);
|
||||
setAreaStats(fallbackStats);
|
||||
refreshUnfilteredAreaCount(selection, fallbackStats.count);
|
||||
setRightPaneTab('area');
|
||||
})
|
||||
.catch((error) => logNonAbortError('Failed to fetch postcode stats', error))
|
||||
.finally(() => setLoadingAreaStats(false));
|
||||
},
|
||||
[resolution, fetchPostcodeStats, fetchHexagonStats, refreshUnfilteredAreaCount]
|
||||
[
|
||||
resolution,
|
||||
fetchPostcodeStats,
|
||||
fetchHexagonStats,
|
||||
fetchPostcodeProperties,
|
||||
refreshUnfilteredAreaCount,
|
||||
]
|
||||
);
|
||||
|
||||
const handleCurrentLocationSearch = useCallback(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import type { PlaceResult } from '../types';
|
||||
import type { AddressResult, PlaceResult } from '../types';
|
||||
import { authHeaders, logNonAbortError } from '../lib/api';
|
||||
|
||||
/** Matches a full UK postcode with complete inward code (e.g. "E14 2DG", "SW1A1AA").
|
||||
|
|
@ -19,8 +19,54 @@ function normalizePostcode(s: string): string {
|
|||
return stripped;
|
||||
}
|
||||
|
||||
function searchableTextForResult(result: SearchResult): string {
|
||||
if (result.type === 'postcode') {
|
||||
return result.label;
|
||||
}
|
||||
if (result.type === 'address') {
|
||||
return `${result.address} ${result.postcode}`;
|
||||
}
|
||||
return `${result.name} ${result.city ?? ''}`;
|
||||
}
|
||||
|
||||
function searchTokens(value: string): string[] {
|
||||
return value.toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
||||
}
|
||||
|
||||
function compactSearchText(value: string): string {
|
||||
return searchTokens(value).join('');
|
||||
}
|
||||
|
||||
function resultMatchesQuery(result: SearchResult, query: string): boolean {
|
||||
const queryTokens = searchTokens(query);
|
||||
if (queryTokens.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const searchable = searchableTextForResult(result);
|
||||
const resultText = searchTokens(searchable).join(' ');
|
||||
const resultCompact = compactSearchText(searchable);
|
||||
const queryCompact = queryTokens.join('');
|
||||
|
||||
return (
|
||||
queryTokens.every((token) => resultText.includes(token)) ||
|
||||
(queryCompact.length >= 2 && resultCompact.includes(queryCompact))
|
||||
);
|
||||
}
|
||||
|
||||
function filterResultsForQuery(results: SearchResult[], query: string): SearchResult[] {
|
||||
return results.filter((result) => resultMatchesQuery(result, query)).slice(0, 20);
|
||||
}
|
||||
|
||||
export type SearchResult =
|
||||
| { type: 'postcode'; label: string }
|
||||
| {
|
||||
type: 'address';
|
||||
address: string;
|
||||
postcode: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
}
|
||||
| {
|
||||
type: 'place';
|
||||
name: string;
|
||||
|
|
@ -38,10 +84,13 @@ export function useLocationSearch(mode?: string) {
|
|||
const [open, setOpen] = useState(false);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const latestQueryRef = useRef('');
|
||||
const lastResultsRef = useRef<SearchResult[]>([]);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(value: string) => {
|
||||
setQuery(value);
|
||||
latestQueryRef.current = value;
|
||||
setActiveIndex(-1);
|
||||
|
||||
abortRef.current?.abort();
|
||||
|
|
@ -50,12 +99,16 @@ export function useLocationSearch(mode?: string) {
|
|||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
setResults([]);
|
||||
lastResultsRef.current = [];
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mode && looksLikePostcode(trimmed)) {
|
||||
setResults([{ type: 'postcode', label: normalizePostcode(trimmed) }]);
|
||||
const postcodeResults: SearchResult[] = [
|
||||
{ type: 'postcode', label: normalizePostcode(trimmed) },
|
||||
];
|
||||
setResults(postcodeResults);
|
||||
setOpen(true);
|
||||
return;
|
||||
}
|
||||
|
|
@ -66,19 +119,27 @@ export function useLocationSearch(mode?: string) {
|
|||
return;
|
||||
}
|
||||
|
||||
const locallyFilteredResults = filterResultsForQuery(lastResultsRef.current, trimmed);
|
||||
setResults(locallyFilteredResults);
|
||||
setOpen(locallyFilteredResults.length > 0);
|
||||
|
||||
debounceRef.current = setTimeout(async () => {
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
try {
|
||||
const params = new URLSearchParams({ q: trimmed, limit: '7' });
|
||||
const params = new URLSearchParams({ q: trimmed, limit: '20' });
|
||||
if (mode) params.set('mode', mode);
|
||||
const res = await fetch(
|
||||
`/api/places?${params}`,
|
||||
authHeaders({ signal: controller.signal })
|
||||
);
|
||||
if (!res.ok) return;
|
||||
const json: { places: PlaceResult[] } = await res.json();
|
||||
const placeResults: SearchResult[] = json.places.map((p) => ({
|
||||
const json: {
|
||||
places: PlaceResult[];
|
||||
postcodes?: string[];
|
||||
addresses?: AddressResult[];
|
||||
} = await res.json();
|
||||
const placeResults = json.places.map((p) => ({
|
||||
type: 'place' as const,
|
||||
name: p.name,
|
||||
slug: p.slug,
|
||||
|
|
@ -87,8 +148,34 @@ export function useLocationSearch(mode?: string) {
|
|||
lon: p.lon,
|
||||
city: p.city === 'City of London' ? 'London' : p.city,
|
||||
}));
|
||||
setResults(placeResults);
|
||||
setOpen(placeResults.length > 0);
|
||||
const outcodeResults = placeResults.filter((result) => result.place_type === 'outcode');
|
||||
const otherPlaceResults = placeResults.filter(
|
||||
(result) => result.place_type !== 'outcode'
|
||||
);
|
||||
const postcodeResults: SearchResult[] = (json.postcodes ?? []).map((postcode) => ({
|
||||
type: 'postcode' as const,
|
||||
label: postcode,
|
||||
}));
|
||||
const addressResults: SearchResult[] = (json.addresses ?? []).map((address) => ({
|
||||
type: 'address' as const,
|
||||
address: address.address,
|
||||
postcode: address.postcode,
|
||||
lat: address.lat,
|
||||
lon: address.lon,
|
||||
}));
|
||||
const containsHouseNumber = /\d/.test(trimmed);
|
||||
const combinedResults = (
|
||||
containsHouseNumber
|
||||
? [...outcodeResults, ...postcodeResults, ...addressResults, ...otherPlaceResults]
|
||||
: [...outcodeResults, ...postcodeResults, ...otherPlaceResults, ...addressResults]
|
||||
).slice(0, 20);
|
||||
if (controller.signal.aborted || latestQueryRef.current.trim() !== trimmed) {
|
||||
return;
|
||||
}
|
||||
lastResultsRef.current = combinedResults;
|
||||
const matchingResults = filterResultsForQuery(combinedResults, trimmed);
|
||||
setResults(matchingResults);
|
||||
setOpen(matchingResults.length > 0);
|
||||
} catch (err) {
|
||||
logNonAbortError('places search', err);
|
||||
}
|
||||
|
|
@ -101,7 +188,9 @@ export function useLocationSearch(mode?: string) {
|
|||
|
||||
const clear = useCallback(() => {
|
||||
setQuery('');
|
||||
latestQueryRef.current = '';
|
||||
setResults([]);
|
||||
lastResultsRef.current = [];
|
||||
setOpen(false);
|
||||
setActiveIndex(-1);
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ export const details: Record<string, Record<string, string>> = {
|
|||
'Distance to nearest park (km)':
|
||||
"Distance à vol d'oiseau en kilomètres depuis le code postal jusqu'à l'entrée du parc la plus proche. Couvre les parcs publics, jardins, terrains de jeux et espaces de loisirs. Utilise les emplacements des points d'accès issus du jeu de données OS Open Greenspace, de sorte que les propriétés bordant un grand parc affichent correctement une courte distance.",
|
||||
'Number of parks within 1km':
|
||||
'Nombre de parcs publics, jardins, terrains de jeux et espaces de loisirs dont au moins une entrée se trouve dans un rayon de 1km du centroïde du code postal de la propri<72><69>té. Dérivé du jeu de données OS Open Greenspace (Ordnance Survey), utilisant les emplacements des entrées de parcs pour une correspondance de proximité précise.',
|
||||
'Nombre de parcs publics, jardins, terrains de jeux et espaces de loisirs dont au moins une entrée se trouve dans un rayon de 1 km du centroïde du code postal de la propriété. Dérivé du jeu de données OS Open Greenspace (Ordnance Survey), utilisant les emplacements des entrées de parcs pour une correspondance de proximité précise.',
|
||||
'Number of restaurants within 2km':
|
||||
'Restaurants, cafés et établissements de restauration dans un rayon de 2km du code postal. Source : OpenStreetMap.',
|
||||
'Number of grocery shops and supermarkets within 2km':
|
||||
|
|
@ -392,7 +392,7 @@ export const details: Record<string, Record<string, string>> = {
|
|||
'% Black': '来自2021年Census。地方政府人口中认同为黑人、英国黑人、加勒比人或非洲人的百分比。',
|
||||
'% East Asian': '来自2021年Census。地方政府人口中认同为华人的百分比。',
|
||||
'% Mixed':
|
||||
'来自2021年Census。地方政府人口中认同为<EFBFBD><EFBFBD>血或多种族群体(白人与黑人加勒比裔、白人与黑人非洲裔、白人与亚洲裔,或其他混血或多种族背景)的百分比。',
|
||||
'来自2021年 Census。地方政府人口中认同为混血或多种族群体(白人与黑人加勒比裔、白人与黑人非洲裔、白人与亚洲裔,或其他混血或多种族背景)的百分比。',
|
||||
'% Other':
|
||||
'来自2021年Census。地方政府人口中认同为其他族裔群体(阿拉伯人或其他未被主要类别涵盖的族裔)的百分比。',
|
||||
'Voter turnout (%)':
|
||||
|
|
@ -406,7 +406,7 @@ export const details: Record<string, Record<string, string>> = {
|
|||
'% Other parties':
|
||||
'该选区中投给工党、保守党、自由民主党、英国改革党和绿党以外政党的有效选票百分比。包括独立候选人、议长和小型政党。',
|
||||
'Distance to nearest park (km)':
|
||||
'从邮政编码到最近公园入口的直线距离(km)。涵盖公共公园、花园、运动<EFBFBD><EFBFBD><EFBFBD>和游乐场地。使用OS Open Greenspace数据集中的出入口位置,因此紧邻大型公园的房产可正确显示较短距离。',
|
||||
'从邮政编码到最近公园入口的直线距离(km)。涵盖公共公园、花园、运动场和游乐场地。使用 OS Open Greenspace 数据集中的出入口位置,因此紧邻大型公园的房产可正确显示较短距离。',
|
||||
'Number of parks within 1km':
|
||||
'以房产邮政编码中心点为圆心,1km半径内至少有一个入口的公共公园、花园、运动场和游乐场地数量。来源于OS Open Greenspace数据集(英国地形测量局),使用公园入口位置进行精确近距离匹配。',
|
||||
'Number of restaurants within 2km':
|
||||
|
|
@ -418,6 +418,146 @@ export const details: Record<string, Record<string, string>> = {
|
|||
'Max available download speed (Mbps)':
|
||||
'来自Ofcom Connected Nations 2025的任意运营商可提供的最大固定宽带下载速度。代表理论最大值,而非实际达到的速度。10 Mbps为基础级,30为超快级,100+为极速级,1000为千兆级。',
|
||||
},
|
||||
hi: {
|
||||
'Property type':
|
||||
'HM Land Registry Price Paid डेटा और EPC प्रमाणपत्रों से लिया गया. अलग मकान, अर्ध-स्वतंत्र मकान, कतारबद्ध मकान (सभी उप-प्रकार सहित), फ्लैट/मेज़ोनेट या अन्य (बंगले, मोबाइल होम आदि).',
|
||||
'Leasehold/Freehold':
|
||||
'HM Land Registry Price Paid डेटा से लिया गया. फ्रीहोल्ड का मतलब है कि भवन और जिस जमीन पर वह है, दोनों आपके हैं. लीजहोल्ड का मतलब है कि भवन आपका है लेकिन जमीन नहीं: फ्रीहोल्डर से निश्चित वर्षों के लिए लीज मिला होता है.',
|
||||
'Last known price':
|
||||
'इस संपत्ति की अंतिम दर्ज बिक्री कीमत HM Land Registry Price Paid डेटा से आती है. यह England की आवासीय बिक्री को कवर करती है. अगर संपत्ति हाल में नहीं बिकी है तो यह कीमत कई साल पुरानी हो सकती है.',
|
||||
'Estimated current price':
|
||||
'अंतिम बिक्री कीमत से शुरू करके स्थानीय कीमत बदलावों के अनुसार repeat-sales index से समायोजित किया गया है (postcode sector और property type के अनुसार ट्रैक किया गया). अगर EPC रिकॉर्ड से बिक्री के बाद नवीनीकरण दिखता है, तो नवीनीकरण प्रीमियम जोड़ा जाता है. हाल की बिक्री मूल कीमत के करीब रहेगी; पुरानी बिक्री में ज्यादा समायोजन होगा.',
|
||||
'Price per sqm':
|
||||
'अंतिम ज्ञात बिक्री कीमत को EPC प्रमाणपत्र में दर्ज कुल फर्श क्षेत्र से भाग देकर निकाला गया. अलग-अलग आकार की संपत्तियों की मूल्य तुलना के लिए उपयोगी. केवल तब उपलब्ध जब कीमत और फर्श क्षेत्र, दोनों डेटा मौजूद हों.',
|
||||
'Est. price per sqm':
|
||||
'मुद्रास्फीति-समायोजित अनुमानित मौजूदा कीमत (किसी नवीनीकरण प्रीमियम सहित) को EPC प्रमाणपत्र में दर्ज कुल फर्श क्षेत्र से भाग देकर निकाला गया. ऐतिहासिक बिक्री कीमत पर आधारित प्रति वर्ग मी कीमत की तुलना में ज्यादा ताजा कीमत/क्षेत्र तुलना देता है.',
|
||||
'Estimated monthly rent':
|
||||
'ONS Price Index of Private Rents (PIPR) से औसत मासिक किराया, जिसे स्थानीय प्राधिकरण और बेडरूम संख्या से मिलाया गया है.',
|
||||
'Total floor area (sqm)':
|
||||
'Energy Performance Certificate (EPC) आकलन के दौरान मापा गया कुल उपयोगी फर्श क्षेत्र, वर्ग मीटर में. सभी रहने योग्य कमरे शामिल, लेकिन गैरेज, बाहरी इमारतें और बाहरी क्षेत्र शामिल नहीं.',
|
||||
'Number of bedrooms & living rooms':
|
||||
'Energy Performance Certificate (EPC) में दर्ज कुल रहने योग्य कमरों की संख्या (बेडरूम और लिविंग रूम). रसोई और बाथरूम आमतौर पर शामिल नहीं होते, जब तक वे रहने योग्य कमरा माने जाने लायक बड़े न हों.',
|
||||
'Construction year':
|
||||
"EPC में दर्ज निर्माण आयु-श्रेणी (जैसे '1930-1949') से मध्य बिंदु लेकर अनुमानित. पुराने भवनों के लिए कम सटीक हो सकता है, जहां आयु-श्रेणी कई दशकों तक फैली होती है.",
|
||||
'Date of last transaction':
|
||||
'इस संपत्ति की सबसे हाल की दर्ज बिक्री तारीख, HM Land Registry Price Paid डेटा से. डेटा में तारीख/समय के रूप में रखी होती है; फिल्टर और चार्ट के लिए fractional year में बदली जाती है.',
|
||||
'Former council house':
|
||||
'Energy Performance Certificate डेटा के TENURE field से निकाला गया. अगर इस संपत्ति के किसी EPC प्रमाणपत्र ने टेन्योर को social rented के रूप में दर्ज किया, तो यह संकेत देता है कि उस निरीक्षण के समय संपत्ति council या housing association stock में थी. बाद में बेची गई संपत्तियां (जैसे Right to Buy के जरिए) यह संकेतक बनाए रखती हैं.',
|
||||
'Current energy rating':
|
||||
'Energy Performance Certificate (EPC) से current energy efficiency rating. A (सबसे efficient) से G (सबसे कम efficient) तक. Property की floor area प्रति energy use पर आधारित.',
|
||||
'Potential energy rating':
|
||||
'Energy Performance Certificate (EPC) से potential energy efficiency rating, अगर EPC report की सभी cost-effective recommended improvements कर दी जाएं. A (सबसे efficient) से G (सबसे कम efficient) तक.',
|
||||
'Interior height (m)':
|
||||
'Energy Performance Certificate (EPC) assessment के दौरान दर्ज average internal floor-to-ceiling height, metres में. Total internal volume को total floor area से divide करके निकाली जाती है.',
|
||||
'Distance to nearest train or tube station (km)':
|
||||
'Postcode से निकटतम railway station या tube/metro/tram stop तक सीधी रेखा में दूरी, kilometres में.',
|
||||
'Good+ primary schools within 2km':
|
||||
'2km के भीतर state-funded primary schools जिनकी current Ofsted rating Good या Outstanding है. जिन schools का अभी inspection नहीं हुआ, वे excluded हैं.',
|
||||
'Good+ secondary schools within 2km':
|
||||
'2km के भीतर state-funded secondary schools जिनकी current Ofsted rating Good या Outstanding है. जिन schools का अभी inspection नहीं हुआ, वे excluded हैं.',
|
||||
'Good+ primary schools within 5km':
|
||||
'5km के भीतर state-funded primary schools जिनकी current Ofsted rating Good या Outstanding है. जिन schools का अभी inspection नहीं हुआ, वे excluded हैं.',
|
||||
'Good+ secondary schools within 5km':
|
||||
'5km के भीतर state-funded secondary schools जिनकी current Ofsted rating Good या Outstanding है. जिन schools का अभी inspection नहीं हुआ, वे excluded हैं.',
|
||||
'Outstanding primary schools within 2km':
|
||||
'2km के भीतर state-funded primary schools जिनकी current Ofsted rating Outstanding है. जिन schools का अभी inspection नहीं हुआ, वे excluded हैं.',
|
||||
'Outstanding secondary schools within 2km':
|
||||
'2km के भीतर state-funded secondary schools जिनकी current Ofsted rating Outstanding है. जिन schools का अभी inspection नहीं हुआ, वे excluded हैं.',
|
||||
'Outstanding primary schools within 5km':
|
||||
'5km के भीतर state-funded primary schools जिनकी current Ofsted rating Outstanding है. जिन schools का अभी inspection नहीं हुआ, वे excluded हैं.',
|
||||
'Outstanding secondary schools within 5km':
|
||||
'5km के भीतर state-funded secondary schools जिनकी current Ofsted rating Outstanding है. जिन schools का अभी inspection नहीं हुआ, वे excluded हैं.',
|
||||
'Education, Skills and Training Score':
|
||||
'English Indices of Deprivation से लिया गया (invert किया गया ताकि higher = better). School attainment, higher education entry, adult qualifications और English language proficiency को cover करता है. Higher scores कम deprivation दिखाते हैं.',
|
||||
'Income Score (rate)':
|
||||
'English Indices of Deprivation से लिया गया (invert किया गया ताकि higher = better). Higher values कम income deprivation दिखाते हैं. Income support, income-based Jobseeker’s Allowance, income-based Employment and Support Allowance, Pension Credit, Working and Child Tax Credit, Universal Credit और asylum seekers पर आधारित.',
|
||||
'Employment Score (rate)':
|
||||
'English Indices of Deprivation से लिया गया (invert किया गया ताकि higher = better). Higher values कम employment deprivation दिखाते हैं. Jobseeker’s Allowance, Employment and Support Allowance, Incapacity Benefit, Severe Disablement Allowance, Carer’s Allowance और relevant Universal Credit claimants पर आधारित.',
|
||||
'Health Deprivation and Disability Score':
|
||||
'English Indices of Deprivation से लिया गया (invert किया गया ताकि higher = better). Higher scores premature death का कम risk और बेहतर quality of life दिखाते हैं. Years of potential life lost, comparative illness and disability ratio, acute morbidity और mood/anxiety disorders से derived.',
|
||||
'Living Environment Score':
|
||||
'English Indices of Deprivation से लिया गया (invert किया गया ताकि higher = better). Housing quality (condition, central heating) और outdoor environment (air quality, road safety) को combine करता है. Higher scores बेहतर living environments दिखाते हैं.',
|
||||
'Indoors Sub-domain Score':
|
||||
'English Indices of Deprivation, Living Environment domain से लिया गया (invert किया गया ताकि higher = better). Housing stock की quality मापता है: central heating availability, housing condition और Decent Homes standards. Higher scores बेहतर housing conditions दिखाते हैं.',
|
||||
'Outdoors Sub-domain Score':
|
||||
'English Indices of Deprivation, Living Environment domain से लिया गया (invert किया गया ताकि higher = better). Air quality indicators और pedestrians/cyclists से जुड़े road traffic accident casualties के जरिए outdoor living environment quality मापता है. Higher scores बेहतर outdoor environments दिखाते हैं.',
|
||||
'Serious crime per 1k residents (avg/yr)':
|
||||
'LSOA में प्रति 1,000 usual residents प्रति वर्ष violence, robbery, burglary और possession of weapons. police.uk street-level crime data (2023-2025) और Census 2021 population counts का उपयोग करता है. Population density normalize करता है ताकि areas size की परवाह किए बिना comparable हों.',
|
||||
'Minor crime per 1k residents (avg/yr)':
|
||||
'LSOA में प्रति 1,000 usual residents प्रति वर्ष anti-social behaviour, shoplifting, bicycle theft और other lower-severity crimes. police.uk street-level crime data (2023-2025) और Census 2021 population counts का उपयोग करता है. Population density normalize करता है ताकि areas comparable हों.',
|
||||
'Serious crime (avg/yr)':
|
||||
'LSOA में प्रति वर्ष violence, robbery, burglary और possession of weapons का sum, police.uk street-level crime data (2023-2025) से. Serious crime का एक single indicator देता है.',
|
||||
'Minor crime (avg/yr)':
|
||||
'LSOA में प्रति वर्ष anti-social behaviour, shoplifting, bicycle theft और other lower-severity crimes का sum, police.uk street-level crime data (2023-2025) से. Minor crime का एक single indicator देता है.',
|
||||
'Violence and sexual offences (avg/yr)':
|
||||
'LSOA में प्रति वर्ष violence और sexual offences की average count, police.uk street-level crime data (2023-2025) से. Assaults, harassment और sexual offences शामिल.',
|
||||
'Burglary (avg/yr)':
|
||||
'LSOA में प्रति वर्ष burglaries की average count, police.uk street-level crime data (2023-2025) से. Residential और commercial burglaries शामिल.',
|
||||
'Robbery (avg/yr)':
|
||||
'LSOA में प्रति वर्ष robberies की average count, police.uk street-level crime data (2023-2025) से. Robbery में force या threat of force के साथ theft शामिल है.',
|
||||
'Vehicle crime (avg/yr)':
|
||||
'LSOA में प्रति वर्ष vehicle-related crime incidents की average count, police.uk street-level crime data (2023-2025) से. Vehicles की theft और vehicles के अंदर से theft शामिल.',
|
||||
'Anti-social behaviour (avg/yr)':
|
||||
'LSOA में प्रति वर्ष anti-social behaviour incidents की average count, police.uk street-level crime data (2023-2025) से. Nuisance, environmental और personal anti-social behaviour शामिल.',
|
||||
'Criminal damage and arson (avg/yr)':
|
||||
'LSOA में प्रति वर्ष criminal damage और arson incidents की average count, police.uk street-level crime data (2023-2025) से.',
|
||||
'Other theft (avg/yr)':
|
||||
"'Other theft' offences की LSOA में प्रति वर्ष average count, police.uk street-level crime data (2023-2025) से. Burglary, vehicle crime, shoplifting या bicycle theft में न आने वाली theft शामिल.",
|
||||
'Theft from the person (avg/yr)':
|
||||
'LSOA में प्रति वर्ष theft-from-the-person offences की average count, police.uk street-level crime data (2023-2025) से. Pickpocketing और बिना force के bag snatching शामिल.',
|
||||
'Shoplifting (avg/yr)':
|
||||
'LSOA में प्रति वर्ष shoplifting offences की average count, police.uk street-level crime data (2023-2025) से.',
|
||||
'Bicycle theft (avg/yr)':
|
||||
'LSOA में प्रति वर्ष bicycle theft offences की average count, police.uk street-level crime data (2023-2025) से.',
|
||||
'Drugs (avg/yr)':
|
||||
'LSOA में प्रति वर्ष drug offences की average count, police.uk street-level crime data (2023-2025) से. Possession और trafficking offences शामिल.',
|
||||
'Possession of weapons (avg/yr)':
|
||||
'LSOA में प्रति वर्ष possession-of-weapons offences की average count, police.uk street-level crime data (2023-2025) से.',
|
||||
'Public order (avg/yr)':
|
||||
'LSOA में प्रति वर्ष public order offences की average count, police.uk street-level crime data (2023-2025) से. Fear, alarm या distress पैदा करने वाले acts शामिल.',
|
||||
'Other crime (avg/yr)':
|
||||
'LSOA में प्रति वर्ष other criminal offences की average count, police.uk street-level crime data (2023-2025) से. कहीं और classified न होने वाले offences के लिए catch-all category.',
|
||||
'Median age':
|
||||
'Census 2021 (TS007A) से. LSOA में usual residents की median age, five-year age band counts से linear interpolation द्वारा calculated. Younger-population areas आमतौर पर urban, university towns या family-heavy होते हैं; higher medians rural और coastal areas में common हैं.',
|
||||
'% White':
|
||||
'Census 2021 से. Local authority population का percentage जो White (English, Welsh, Scottish, Northern Irish, British, Irish, Gypsy or Irish Traveller, Roma या any other white background) के रूप में identify करता है.',
|
||||
'% South Asian':
|
||||
'Census 2021 से. Local authority population का percentage जो Indian, Pakistani, Bangladeshi या any other Asian background के रूप में identify करता है.',
|
||||
'% Black':
|
||||
'Census 2021 से. Local authority population का percentage जो Black, Black British, Caribbean या African के रूप में identify करता है.',
|
||||
'% East Asian':
|
||||
'Census 2021 से. Local authority population का percentage जो Chinese के रूप में identify करता है.',
|
||||
'% Mixed':
|
||||
'Census 2021 से. Local authority population का percentage जो Mixed या multiple ethnic groups (White and Black Caribbean, White and Black African, White and Asian या other mixed/multiple background) के रूप में identify करता है.',
|
||||
'% Other':
|
||||
'Census 2021 से. Local authority population का percentage जो another ethnic group (Arab या main categories से बाहर any other ethnic group) के रूप में identify करता है.',
|
||||
'Voter turnout (%)':
|
||||
'July 2024 UK General Election में valid vote देने वाले registered electorate का proportion. Valid votes divided by electorate size. Higher turnout अक्सर affluent areas और closer contests से correlate करता है.',
|
||||
'% Labour':
|
||||
'July 2024 UK General Election में इस postcode को cover करने वाली constituency में Labour Party candidates को पड़े valid votes का percentage.',
|
||||
'% Conservative':
|
||||
'July 2024 UK General Election में इस postcode को cover करने वाली constituency में Conservative Party को पड़े valid votes का percentage.',
|
||||
'% Liberal Democrat':
|
||||
'July 2024 UK General Election में इस postcode को cover करने वाली constituency में Liberal Democrats को पड़े valid votes का percentage.',
|
||||
'% Reform UK':
|
||||
'July 2024 UK General Election में इस postcode को cover करने वाली constituency में Reform UK को पड़े valid votes का percentage.',
|
||||
'% Green':
|
||||
'July 2024 UK General Election में इस postcode को cover करने वाली constituency में Green Party को पड़े valid votes का percentage.',
|
||||
'% Other parties':
|
||||
'इस postcode की constituency में Labour, Conservative, Liberal Democrat, Reform UK और Green के अलावा अन्य parties को पड़े valid votes का percentage. Independents, Speaker और minor parties शामिल.',
|
||||
'Distance to nearest park (km)':
|
||||
'Postcode से nearest park entrance तक सीधी रेखा में दूरी, kilometres में. Public parks, gardens, playing fields और play spaces को cover करता है. OS Open Greenspace dataset के access-point locations का उपयोग करता है, इसलिए बड़े park के किनारे properties सही short distance दिखाती हैं.',
|
||||
'Number of parks within 1km':
|
||||
'Property postcode centroid से 1km radius के भीतर कम से कम एक entrance रखने वाले public parks, gardens, playing fields और play spaces की संख्या. OS Open Greenspace dataset (Ordnance Survey) से derived, accurate proximity matching के लिए park entrance locations उपयोग करता है.',
|
||||
'Number of restaurants within 2km':
|
||||
'Postcode से 2km के भीतर restaurants, cafés और food-service venues. Source: OpenStreetMap.',
|
||||
'Number of grocery shops and supermarkets within 2km':
|
||||
'Property postcode centroid से 2km radius के भीतर supermarkets, convenience stores और other grocery shops की संख्या. OpenStreetMap POI data से derived.',
|
||||
'Noise (dB)':
|
||||
'Defra Strategic Noise Mapping Round 4 (2022) से road-noise level in decibels (Lden, 24-hour weighted average). Ground से 4m ऊपर 10m grid पर modelled. लगभग 55 dB से ऊपर शोर आमतौर पर noticeable होता है; लगभग 70 dB से ऊपर WHO harmful मानता है.',
|
||||
'Max available download speed (Mbps)':
|
||||
'Ofcom Connected Nations 2025 से किसी भी provider द्वारा उपलब्ध maximum fixed-broadband download speed. Theoretical maximum दर्शाता है, achieved speed नहीं. 10 Mbps = basic, 30 = superfast, 100+ = ultrafast, 1000 = gigabit.',
|
||||
},
|
||||
hu: {
|
||||
'Property type':
|
||||
'Az HM Land Registry Price Paid adatokból és EPC tanúsítványokból. Különálló, Ikerház, Sorház (minden sorház altípust tartalmaz), Lakás/Maisonette, vagy Egyéb (bungaló, mobilház stb.).',
|
||||
|
|
|
|||
|
|
@ -23,11 +23,6 @@ html.dark {
|
|||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.home-page-scroll,
|
||||
.dark .home-page-scroll {
|
||||
background: linear-gradient(180deg, #080d19 0%, #080d19 50%, #16a34a 50%, #16a34a 100%);
|
||||
}
|
||||
|
||||
/* Smooth theme transitions (scoped to avoid map performance issues) */
|
||||
body,
|
||||
div,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
||||
const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
|
||||
const sharp = require('sharp');
|
||||
|
||||
const HOUSE_IMAGE_WIDTH = 260;
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const isProduction = argv.mode === 'production';
|
||||
|
|
@ -12,7 +15,8 @@ module.exports = (env, argv) => {
|
|||
entry: './src/index.tsx',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'bundle.js',
|
||||
filename: isProduction ? '[name].[contenthash:8].js' : 'bundle.js',
|
||||
chunkFilename: isProduction ? '[name].[contenthash:8].js' : '[name].bundle.js',
|
||||
clean: true,
|
||||
|
||||
publicPath: '/',
|
||||
|
|
@ -52,7 +56,34 @@ module.exports = (env, argv) => {
|
|||
template: './src/index.html',
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [{ from: 'public', noErrorOnMissing: true }],
|
||||
patterns: [
|
||||
{
|
||||
from: 'public',
|
||||
noErrorOnMissing: true,
|
||||
globOptions: {
|
||||
ignore: ['**/house.png'],
|
||||
},
|
||||
},
|
||||
isProduction
|
||||
? {
|
||||
from: 'public/house.png',
|
||||
to: 'house.png',
|
||||
noErrorOnMissing: true,
|
||||
transform: {
|
||||
transformer(content) {
|
||||
return sharp(content)
|
||||
.resize({ width: HOUSE_IMAGE_WIDTH, withoutEnlargement: true })
|
||||
.png({ compressionLevel: 9, palette: true, quality: 85 })
|
||||
.toBuffer();
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
from: 'public/house.png',
|
||||
to: 'house.png',
|
||||
noErrorOnMissing: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
new FaviconsWebpackPlugin({
|
||||
logo: './public/favicon.svg',
|
||||
|
|
@ -69,9 +100,111 @@ module.exports = (env, argv) => {
|
|||
},
|
||||
}),
|
||||
...(isProduction
|
||||
? [new MiniCssExtractPlugin()]
|
||||
? [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].[contenthash:8].css',
|
||||
chunkFilename: '[name].[contenthash:8].css',
|
||||
}),
|
||||
]
|
||||
: [new ReactRefreshWebpackPlugin()]),
|
||||
],
|
||||
optimization: isProduction
|
||||
? {
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
cacheGroups: {
|
||||
maplibre: {
|
||||
test: /[\\/]node_modules[\\/]maplibre-gl[\\/]/,
|
||||
name: 'vendor-maplibre',
|
||||
chunks: 'async',
|
||||
priority: 50,
|
||||
enforce: true,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
h3: {
|
||||
test: /[\\/]node_modules[\\/]h3-js[\\/]/,
|
||||
name: 'vendor-h3',
|
||||
chunks: 'async',
|
||||
priority: 45,
|
||||
enforce: true,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
deckMapbox: {
|
||||
test: /[\\/]node_modules[\\/]@deck\.gl[\\/]mapbox[\\/]/,
|
||||
name: 'vendor-deck-mapbox',
|
||||
chunks: 'async',
|
||||
priority: 44,
|
||||
enforce: true,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
deckCore: {
|
||||
test: /[\\/]node_modules[\\/]@deck\.gl[\\/]core[\\/]/,
|
||||
name: 'vendor-deck-core',
|
||||
chunks: 'async',
|
||||
priority: 43,
|
||||
enforce: true,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
deckLayers: {
|
||||
test: /[\\/]node_modules[\\/]@deck\.gl[\\/](?:layers|geo-layers)[\\/]/,
|
||||
name: 'vendor-deck-layers',
|
||||
chunks: 'async',
|
||||
priority: 42,
|
||||
enforce: true,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
deck: {
|
||||
test: /[\\/]node_modules[\\/]@deck\.gl[\\/]/,
|
||||
name: 'vendor-deck',
|
||||
chunks: 'async',
|
||||
priority: 40,
|
||||
enforce: true,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
luma: {
|
||||
test: /[\\/]node_modules[\\/]@luma\.gl[\\/]/,
|
||||
name: 'vendor-luma',
|
||||
chunks: 'async',
|
||||
priority: 35,
|
||||
enforce: true,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
webgpuSupport: {
|
||||
test: /[\\/]node_modules[\\/]wgsl_reflect[\\/]/,
|
||||
name: 'vendor-webgpu-support',
|
||||
chunks: 'async',
|
||||
priority: 34,
|
||||
enforce: true,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
mapData: {
|
||||
test: /[\\/]node_modules[\\/](?:@mapbox[\\/]tiny-sdf|@protomaps[\\/]basemaps|earcut|supercluster)[\\/]/,
|
||||
name: 'vendor-map-data',
|
||||
chunks: 'async',
|
||||
priority: 33,
|
||||
enforce: true,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
mapSupport: {
|
||||
test: /[\\/]node_modules[\\/](?:@loaders\.gl|@math\.gl|@probe\.gl|@vis\.gl|mjolnir\.js|react-map-gl)[\\/]/,
|
||||
name: 'vendor-map-support',
|
||||
chunks: 'async',
|
||||
priority: 30,
|
||||
enforce: true,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
joyride: {
|
||||
test: /[\\/]node_modules[\\/](?:react-joyride|deepmerge|scroll|scrollparent|react-innertext)[\\/]/,
|
||||
name: 'vendor-joyride',
|
||||
chunks: 'async',
|
||||
priority: 25,
|
||||
enforce: true,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
devServer: {
|
||||
host: '0.0.0.0',
|
||||
port: 3001,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""Extract place=* nodes and railway stations from OSM PBF → data/places.parquet.
|
||||
|
||||
Extracts named place nodes (cities, towns, suburbs, etc.) and railway stations
|
||||
(tube, national rail, DLR, etc.) for typeahead search.
|
||||
Extracts named place nodes and railway stations (tube, national rail, DLR,
|
||||
etc.) for typeahead search.
|
||||
Reuses the same england-latest.osm.pbf as pois.py.
|
||||
"""
|
||||
|
||||
|
|
@ -21,7 +21,22 @@ from pipeline.utils.england_geometry import (
|
|||
load_england_polygon,
|
||||
)
|
||||
|
||||
PLACE_TYPES = {"city"}
|
||||
# Search can use a wider set of OSM place nodes, but travel-time destinations
|
||||
# must remain restricted to the historical city/station origin set.
|
||||
SEARCH_PLACE_TYPES = {
|
||||
"city",
|
||||
"town",
|
||||
"village",
|
||||
"suburb",
|
||||
"neighbourhood",
|
||||
"quarter",
|
||||
"borough",
|
||||
"locality",
|
||||
"hamlet",
|
||||
"isolated_dwelling",
|
||||
"island",
|
||||
}
|
||||
TRAVEL_DESTINATION_PLACE_TYPES = {"city"}
|
||||
|
||||
# Suffixes to strip from raw station names before appending the typed suffix.
|
||||
_STATION_STRIP = (
|
||||
|
|
@ -71,7 +86,13 @@ class PlaceHandler(osmium.SimpleHandler):
|
|||
self._england = england_polygon
|
||||
|
||||
def _add(
|
||||
self, name: str, place_type: str, lat: float, lon: float, population: int
|
||||
self,
|
||||
name: str,
|
||||
place_type: str,
|
||||
lat: float,
|
||||
lon: float,
|
||||
population: int,
|
||||
travel_destination: bool,
|
||||
) -> None:
|
||||
self.places.append(
|
||||
{
|
||||
|
|
@ -80,6 +101,7 @@ class PlaceHandler(osmium.SimpleHandler):
|
|||
"lat": lat,
|
||||
"lon": lon,
|
||||
"population": population,
|
||||
"travel_destination": travel_destination,
|
||||
}
|
||||
)
|
||||
self._progress.set_postfix(places=f"{len(self.places):,}", refresh=False)
|
||||
|
|
@ -107,10 +129,17 @@ class PlaceHandler(osmium.SimpleHandler):
|
|||
except ValueError:
|
||||
population = 0
|
||||
|
||||
# place=* nodes (cities, towns, suburbs, etc.)
|
||||
# place=* nodes
|
||||
place_type = n.tags.get("place")
|
||||
if place_type in PLACE_TYPES:
|
||||
self._add(name, place_type, lat, lon, population)
|
||||
if place_type in SEARCH_PLACE_TYPES:
|
||||
self._add(
|
||||
name,
|
||||
place_type,
|
||||
lat,
|
||||
lon,
|
||||
population,
|
||||
travel_destination=place_type in TRAVEL_DESTINATION_PLACE_TYPES,
|
||||
)
|
||||
return
|
||||
|
||||
# Railway stations (tube, national rail, DLR, overground, Elizabeth line)
|
||||
|
|
@ -126,7 +155,14 @@ class PlaceHandler(osmium.SimpleHandler):
|
|||
):
|
||||
return
|
||||
display_name = _station_display_name(name, tags)
|
||||
self._add(display_name, "station", lat, lon, population)
|
||||
self._add(
|
||||
display_name,
|
||||
"station",
|
||||
lat,
|
||||
lon,
|
||||
population,
|
||||
travel_destination=True,
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
|
|
@ -147,7 +183,7 @@ def main() -> None:
|
|||
pbf_file = args.pbf
|
||||
england_polygon = load_england_polygon(args.boundary)
|
||||
|
||||
print("Extracting place nodes: cities + railway stations")
|
||||
print("Extracting search place nodes + railway stations")
|
||||
with tqdm(
|
||||
unit=" elements",
|
||||
unit_scale=True,
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ public class App {
|
|||
String[] originNames = places.names();
|
||||
double[] originLats = places.lats(), originLons = places.lons();
|
||||
int nOrigins = originLats.length;
|
||||
System.err.printf(" %,d places (total)%n", nOrigins);
|
||||
System.err.printf(" %,d travel-eligible places%n", nOrigins);
|
||||
|
||||
// Filter places to England only (must be near at least one England postcode)
|
||||
Set<Integer> englandIndices = filterEnglandPlaces(
|
||||
|
|
@ -89,7 +89,7 @@ public class App {
|
|||
System.err.printf("DEMO MODE: %d places (transit only)%n", originIndices.length);
|
||||
for (int i : originIndices) System.err.printf(" - %s%n", originNames[i]);
|
||||
} else {
|
||||
// Normal mode: use all England places
|
||||
// Normal mode: use all travel-eligible England places
|
||||
originIndices = englandIndices.stream().sorted()
|
||||
.mapToInt(Integer::intValue).toArray();
|
||||
modes = MODES;
|
||||
|
|
|
|||
|
|
@ -59,9 +59,15 @@ public class Parquet {
|
|||
/** Load places deduplicated by lat/lon, write reference parquet, return names + flat lat/lon arrays. */
|
||||
static Places loadPlaces(String parquetPath, Path refOut) throws Exception {
|
||||
try (DuckDBConnection conn = connect(); Statement stmt = conn.createStatement()) {
|
||||
stmt.execute("CREATE TABLE all_places AS SELECT * FROM read_parquet('"
|
||||
+ escapePath(parquetPath) + "')");
|
||||
boolean hasTravelDestination = tableHasColumn(stmt, "all_places", "travel_destination");
|
||||
String source = hasTravelDestination
|
||||
? "(SELECT * FROM all_places WHERE COALESCE(travel_destination, true))"
|
||||
: "all_places";
|
||||
stmt.execute("CREATE TABLE places AS SELECT * EXCLUDE (rn) FROM ("
|
||||
+ "SELECT *, ROW_NUMBER() OVER (PARTITION BY lat, lon) AS rn "
|
||||
+ "FROM read_parquet('" + escapePath(parquetPath) + "')) WHERE rn = 1");
|
||||
+ "FROM " + source + " AS p) WHERE rn = 1");
|
||||
copyToParquet(stmt, "SELECT * FROM places", refOut);
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM places")) {
|
||||
|
|
@ -85,6 +91,17 @@ public class Parquet {
|
|||
}
|
||||
}
|
||||
|
||||
private static boolean tableHasColumn(Statement stmt, String tableName, String columnName)
|
||||
throws Exception {
|
||||
try (ResultSet rs = stmt.executeQuery(
|
||||
"SELECT COUNT(*) FROM information_schema.columns "
|
||||
+ "WHERE table_name = '" + tableName + "' "
|
||||
+ "AND column_name = '" + columnName + "'")) {
|
||||
rs.next();
|
||||
return rs.getInt(1) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Write postcode travel times as a ZSTD-compressed parquet (atomic via tmp + rename). */
|
||||
static void writeTravelTimes(DuckDBConnection conn, Path outPath, String[] postcodes, short[] times)
|
||||
throws Exception {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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::{
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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![];
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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&path={path_e}")
|
||||
} else {
|
||||
format!("{public_url_e}/api/screenshot?og=1&path={path_e}&{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&{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&path={path_e}")
|
||||
} else {
|
||||
format!("{public_url_e}/api/screenshot?og=1&path={path_e}&{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&{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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue