.
This commit is contained in:
parent
f99bd4e5c9
commit
a04ac2d857
16 changed files with 132 additions and 74 deletions
|
|
@ -327,8 +327,8 @@ $(GREENSPACE): $(PBF)
|
||||||
$(OS_GREENSPACE):
|
$(OS_GREENSPACE):
|
||||||
uv run python -m pipeline.download.os_greenspace --output $@
|
uv run python -m pipeline.download.os_greenspace --output $@
|
||||||
|
|
||||||
$(PLACES): $(PBF) $(ENGLAND_BOUNDARY) $(NAPTAN) $(OFS_REGISTER) $(ARCGIS)
|
$(PLACES): $(PBF) $(ENGLAND_BOUNDARY) $(NAPTAN) $(OFS_REGISTER) $(ARCGIS) $(POIS_RAW)
|
||||||
uv run python -m pipeline.download.places --output $@ --pbf $(PBF) --boundary $(ENGLAND_BOUNDARY) --naptan $(NAPTAN) --university-register $(OFS_REGISTER) --postcodes $(ARCGIS)
|
uv run python -m pipeline.download.places --output $@ --pbf $(PBF) --boundary $(ENGLAND_BOUNDARY) --naptan $(NAPTAN) --university-register $(OFS_REGISTER) --postcodes $(ARCGIS) --pois $(POIS_RAW) --include-streets
|
||||||
|
|
||||||
|
|
||||||
$(MEDIAN_AGE):
|
$(MEDIAN_AGE):
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,69 @@ describe('LocationSearch', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows an empty state for invalid place queries', async () => {
|
it('preserves the server unified ordering and sends the viewport centre', async () => {
|
||||||
|
const fetchMock = vi.fn((input: string | URL | Request) => {
|
||||||
|
const url = new URL(String(input), 'http://localhost');
|
||||||
|
if (url.pathname === '/api/places') {
|
||||||
|
return Promise.resolve(
|
||||||
|
jsonResponse({
|
||||||
|
places: [],
|
||||||
|
postcodes: [],
|
||||||
|
addresses: [],
|
||||||
|
// Intentionally out of "bucket" order: an address outranks a place. The hook must
|
||||||
|
// render them in this server order rather than re-bucketing or re-filtering.
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
type: 'address',
|
||||||
|
address: '1 High Street',
|
||||||
|
postcode: 'CR0 1AA',
|
||||||
|
lat: 51.37,
|
||||||
|
lon: -0.1,
|
||||||
|
score: 930,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'place',
|
||||||
|
name: 'Croydon',
|
||||||
|
slug: 'croydon',
|
||||||
|
place_type: 'town',
|
||||||
|
lat: 51.37,
|
||||||
|
lon: -0.1,
|
||||||
|
score: 880,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.resolve(new Response(null, { status: 404 }));
|
||||||
|
});
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<LocationSearch
|
||||||
|
onFlyTo={vi.fn()}
|
||||||
|
onLocationSearched={vi.fn()}
|
||||||
|
getViewportCenter={() => ({ lat: 51.5, lng: -0.12 })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'high street' } });
|
||||||
|
|
||||||
|
// The address (first in the server list) renders before the place, unfiltered.
|
||||||
|
const firstResult = await screen.findByText('1 High Street');
|
||||||
|
const place = screen.getByText('Croydon');
|
||||||
|
expect(
|
||||||
|
firstResult.compareDocumentPosition(place) & Node.DOCUMENT_POSITION_FOLLOWING
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
const placesCall = fetchMock.mock.calls.find(([input]) =>
|
||||||
|
String(input).includes('/api/places')
|
||||||
|
);
|
||||||
|
const calledUrl = new URL(String(placesCall?.[0]), 'http://localhost');
|
||||||
|
expect(calledUrl.searchParams.get('lat')).toBe('51.5');
|
||||||
|
expect(calledUrl.searchParams.get('lng')).toBe('-0.12');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows an empty state for invalid place queries (legacy server, no results key)', async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
'fetch',
|
'fetch',
|
||||||
vi.fn(() =>
|
vi.fn(() =>
|
||||||
|
|
@ -314,6 +376,23 @@ describe('LocationSearch', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows the empty state when the new server returns an empty results array', async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn(() =>
|
||||||
|
Promise.resolve(jsonResponse({ places: [], postcodes: [], addresses: [], results: [] }))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<LocationSearch onFlyTo={vi.fn()} onLocationSearched={vi.fn()} />);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'zzzznowhere' } });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('No matching places or postcodes')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps only the three most recent local searches', async () => {
|
it('keeps only the three most recent local searches', async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
'fetch',
|
'fetch',
|
||||||
|
|
|
||||||
|
|
@ -937,6 +937,10 @@ export default memo(function Map({
|
||||||
dimensions.width < DESKTOP_TOP_CARDS_ROW_MIN_MAP_WIDTH;
|
dimensions.width < DESKTOP_TOP_CARDS_ROW_MIN_MAP_WIDTH;
|
||||||
const showLocationSearch = !hideLocationSearch && !hideDesktopTopCardsForWidth;
|
const showLocationSearch = !hideLocationSearch && !hideDesktopTopCardsForWidth;
|
||||||
const showLegend = !hideLegend && !hideDesktopTopCardsForWidth;
|
const showLegend = !hideLegend && !hideDesktopTopCardsForWidth;
|
||||||
|
const getViewportCenter = useCallback(() => {
|
||||||
|
const center = mapRef.current?.getCenter();
|
||||||
|
return center ? { lat: center.lat, lng: center.lng } : null;
|
||||||
|
}, []);
|
||||||
const desktopTopCardsLayoutClass = stackDesktopTopCards
|
const desktopTopCardsLayoutClass = stackDesktopTopCards
|
||||||
? 'flex-col items-start'
|
? 'flex-col items-start'
|
||||||
: 'items-start justify-between';
|
: 'items-start justify-between';
|
||||||
|
|
@ -1099,6 +1103,7 @@ export default memo(function Map({
|
||||||
onLocationSearched={onLocationSearched}
|
onLocationSearched={onLocationSearched}
|
||||||
onCurrentLocationFound={onCurrentLocationFound}
|
onCurrentLocationFound={onCurrentLocationFound}
|
||||||
onMouseEnter={handleMouseLeave}
|
onMouseEnter={handleMouseLeave}
|
||||||
|
getViewportCenter={getViewportCenter}
|
||||||
className={DESKTOP_TOP_CARD_CLASS}
|
className={DESKTOP_TOP_CARD_CLASS}
|
||||||
inputClassName={DESKTOP_LOCATION_SEARCH_INPUT_CLASS}
|
inputClassName={DESKTOP_LOCATION_SEARCH_INPUT_CLASS}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,10 @@ export default function MobileDrawer({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-tutorial="right-pane" className="pointer-events-none fixed inset-0 z-50 flex flex-col">
|
<div
|
||||||
|
data-tutorial="right-pane"
|
||||||
|
className="pointer-events-none fixed inset-0 z-50 flex flex-col"
|
||||||
|
>
|
||||||
<div className="h-[10%] shrink-0" aria-hidden="true" />
|
<div className="h-[10%] shrink-0" aria-hidden="true" />
|
||||||
|
|
||||||
{/* Panel — bottom 90% */}
|
{/* Panel — bottom 90% */}
|
||||||
|
|
|
||||||
|
|
@ -192,9 +192,7 @@ export function useExportController({
|
||||||
const detail = err instanceof Error && err.message.trim() ? ` ${err.message}` : '';
|
const detail = err instanceof Error && err.message.trim() ? ` ${err.message}` : '';
|
||||||
showExportNotice({
|
showExportNotice({
|
||||||
kind: 'error',
|
kind: 'error',
|
||||||
message: timedOut
|
message: timedOut ? t('header.exportTimedOut') : `${t('header.exportFailed')}${detail}`,
|
||||||
? t('header.exportTimedOut')
|
|
||||||
: `${t('header.exportFailed')}${detail}`,
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
window.clearTimeout(timeoutId);
|
window.clearTimeout(timeoutId);
|
||||||
|
|
|
||||||
|
|
@ -78,11 +78,7 @@ export default function ExportMenu({ open, exporting, onClose, onExport }: Expor
|
||||||
} else {
|
} else {
|
||||||
inputRefs.current[idx + 1]?.focus();
|
inputRefs.current[idx + 1]?.focus();
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (e.key === 'Backspace' && postcodes[idx] === '' && postcodes.length > 1) {
|
||||||
e.key === 'Backspace' &&
|
|
||||||
postcodes[idx] === '' &&
|
|
||||||
postcodes.length > 1
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
focusIndexRef.current = Math.max(0, idx - 1);
|
focusIndexRef.current = Math.max(0, idx - 1);
|
||||||
removeAt(idx);
|
removeAt(idx);
|
||||||
|
|
@ -98,11 +94,7 @@ export default function ExportMenu({ open, exporting, onClose, onExport }: Expor
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div className="fixed inset-0 bg-black/50 z-[90]" onClick={onClose} aria-hidden="true" />
|
||||||
className="fixed inset-0 bg-black/50 z-[90]"
|
|
||||||
onClick={onClose}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,7 @@ export function FeatureLabel({
|
||||||
{GroupIcon && <GroupIcon className={iconClass} />}
|
{GroupIcon && <GroupIcon className={iconClass} />}
|
||||||
{translatedDesc ? (
|
{translatedDesc ? (
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className={`flex ${wrap ? 'items-start' : 'items-center'} gap-1`}>
|
<div className={`flex ${wrap ? 'items-start' : 'items-center'} gap-1`}>{nameContent}</div>
|
||||||
{nameContent}
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-warm-400 dark:text-warm-500 block">{translatedDesc}</span>
|
<span className="text-xs text-warm-400 dark:text-warm-500 block">{translatedDesc}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -144,12 +144,7 @@ export function useHexagonSelection({
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchPostcodeStats = useCallback(
|
const fetchPostcodeStats = useCallback(
|
||||||
async (
|
async (postcode: string, signal?: AbortSignal, includeFilters = true, fields?: string[]) => {
|
||||||
postcode: string,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
includeFilters = true,
|
|
||||||
fields?: string[]
|
|
||||||
) => {
|
|
||||||
const params = new URLSearchParams({ postcode });
|
const params = new URLSearchParams({ postcode });
|
||||||
const filterStr = includeFilters ? buildFilterString(filters, features) : '';
|
const filterStr = includeFilters ? buildFilterString(filters, features) : '';
|
||||||
if (filterStr) params.append('filters', filterStr);
|
if (filterStr) params.append('filters', filterStr);
|
||||||
|
|
|
||||||
|
|
@ -214,22 +214,19 @@ const de: Translations = {
|
||||||
'Relax one constraint at a time': 'Lockere eine Einschränkung nach der anderen',
|
'Relax one constraint at a time': 'Lockere eine Einschränkung nach der anderen',
|
||||||
'When the search becomes too narrow, loosen a single filter and watch which postcodes reappear. This makes compromise explicit instead of relying on guesswork.':
|
'When the search becomes too narrow, loosen a single filter and watch which postcodes reappear. This makes compromise explicit instead of relying on guesswork.':
|
||||||
'Wenn die Suche zu eng wird, lockere einen einzelnen Filter und sieh, welche Postcodes wieder auftauchen. So werden Kompromisse sichtbar statt geraten.',
|
'Wenn die Suche zu eng wird, lockere einen einzelnen Filter und sieh, welche Postcodes wieder auftauchen. So werden Kompromisse sichtbar statt geraten.',
|
||||||
'Turn vague areas into specific postcodes':
|
'Turn vague areas into specific postcodes': 'Verwandle vage Gebiete in konkrete Postcodes',
|
||||||
'Verwandle vage Gebiete in konkrete Postcodes',
|
|
||||||
'Broad town or borough searches hide large differences between streets. Perfect Postcode helps you move from a general area to postcodes that satisfy your hard requirements.':
|
'Broad town or borough searches hide large differences between streets. Perfect Postcode helps you move from a general area to postcodes that satisfy your hard requirements.':
|
||||||
'Breite Stadt- oder Borough-Suchen verdecken große Unterschiede zwischen Straßen. Perfect Postcode hilft dir, von einem allgemeinen Gebiet zu Postcodes zu gelangen, die deine Muss-Kriterien erfüllen.',
|
'Breite Stadt- oder Borough-Suchen verdecken große Unterschiede zwischen Straßen. Perfect Postcode hilft dir, von einem allgemeinen Gebiet zu Postcodes zu gelangen, die deine Muss-Kriterien erfüllen.',
|
||||||
'Keep trade-offs visible': 'Halte Kompromisse sichtbar',
|
'Keep trade-offs visible': 'Halte Kompromisse sichtbar',
|
||||||
'When there are too many or too few matches, adjust one constraint at a time and see exactly which postcodes reappear. That makes compromises explicit instead of relying on guesswork.':
|
'When there are too many or too few matches, adjust one constraint at a time and see exactly which postcodes reappear. That makes compromises explicit instead of relying on guesswork.':
|
||||||
'Wenn es zu viele oder zu wenige Treffer gibt, ändere jeweils eine Anforderung und sieh genau, welche Postcodes wieder auftauchen. So werden Kompromisse sichtbar statt geraten.',
|
'Wenn es zu viele oder zu wenige Treffer gibt, ändere jeweils eine Anforderung und sieh genau, welche Postcodes wieder auftauchen. So werden Kompromisse sichtbar statt geraten.',
|
||||||
'Why postcode-level comparison matters':
|
'Why postcode-level comparison matters': 'Warum Vergleiche auf Postcode-Ebene wichtig sind',
|
||||||
'Warum Vergleiche auf Postcode-Ebene wichtig sind',
|
|
||||||
'Two nearby postcodes can differ on schools, road noise, transport access, property mix, and price. Comparing at postcode level reduces the chance of treating a whole town as one uniform market.':
|
'Two nearby postcodes can differ on schools, road noise, transport access, property mix, and price. Comparing at postcode level reduces the chance of treating a whole town as one uniform market.':
|
||||||
'Zwei nahegelegene Postcodes können sich bei Schulen, Straßenlärm, Verkehrsanbindung, Immobilienmix und Preis stark unterscheiden. Der Vergleich auf Postcode-Ebene verhindert, dass eine ganze Stadt wie ein einheitlicher Markt behandelt wird.',
|
'Zwei nahegelegene Postcodes können sich bei Schulen, Straßenlärm, Verkehrsanbindung, Immobilienmix und Preis stark unterscheiden. Der Vergleich auf Postcode-Ebene verhindert, dass eine ganze Stadt wie ein einheitlicher Markt behandelt wird.',
|
||||||
'How to use the results': 'So nutzt du die Ergebnisse',
|
'How to use the results': 'So nutzt du die Ergebnisse',
|
||||||
'Treat matching postcodes as a research queue: check live listings, visit streets, confirm schools and admissions, and review current official sources.':
|
'Treat matching postcodes as a research queue: check live listings, visit streets, confirm schools and admissions, and review current official sources.':
|
||||||
'Behandle passende Postcodes wie eine Recherche-Liste: Prüfe Live-Inserate, geh durch die Straßen, bestätige Schulen und Admissions und nutze aktuelle offizielle Quellen.',
|
'Behandle passende Postcodes wie eine Recherche-Liste: Prüfe Live-Inserate, geh durch die Straßen, bestätige Schulen und Admissions und nutze aktuelle offizielle Quellen.',
|
||||||
'Can I save a postcode property search?':
|
'Can I save a postcode property search?': 'Kann ich eine Postcode-Immobiliensuche speichern?',
|
||||||
'Kann ich eine Postcode-Immobiliensuche speichern?',
|
|
||||||
'Yes. Licensed users can save searches and return to them later. Saved searches are designed for shortlists and comparison notes.':
|
'Yes. Licensed users can save searches and return to them later. Saved searches are designed for shortlists and comparison notes.':
|
||||||
'Ja. Lizenzierte Nutzer können Suchen speichern und später darauf zurückgreifen. Gespeicherte Suchen sind für Auswahllisten und Vergleichsnotizen gedacht.',
|
'Ja. Lizenzierte Nutzer können Suchen speichern und später darauf zurückgreifen. Gespeicherte Suchen sind für Auswahllisten und Vergleichsnotizen gedacht.',
|
||||||
'Can I search without knowing the area?': 'Kann ich suchen, ohne die Gegend zu kennen?',
|
'Can I search without knowing the area?': 'Kann ich suchen, ohne die Gegend zu kennen?',
|
||||||
|
|
@ -268,8 +265,7 @@ const de: Translations = {
|
||||||
'Pendeln nach Postcode, nicht nur nach Ortsname',
|
'Pendeln nach Postcode, nicht nur nach Ortsname',
|
||||||
'Two streets in the same town can have very different station access, road routes, and public transport options. Postcode-level travel-time filtering keeps that difference visible.':
|
'Two streets in the same town can have very different station access, road routes, and public transport options. Postcode-level travel-time filtering keeps that difference visible.':
|
||||||
'Zwei Straßen in derselben Stadt können sehr unterschiedliche Bahnhofsanbindungen, Straßenrouten und ÖPNV-Optionen haben. Fahrzeitfilter auf Postcode-Ebene halten diesen Unterschied sichtbar.',
|
'Zwei Straßen in derselben Stadt können sehr unterschiedliche Bahnhofsanbindungen, Straßenrouten und ÖPNV-Optionen haben. Fahrzeitfilter auf Postcode-Ebene halten diesen Unterschied sichtbar.',
|
||||||
'Balance journey time with the rest of the move':
|
'Balance journey time with the rest of the move': 'Fahrzeit mit dem restlichen Umzug abwägen',
|
||||||
'Fahrzeit mit dem restlichen Umzug abwägen',
|
|
||||||
'A fast commute only helps if the area also fits your budget, housing needs, school preferences, safety threshold, broadband requirement, and tolerance for road noise.':
|
'A fast commute only helps if the area also fits your budget, housing needs, school preferences, safety threshold, broadband requirement, and tolerance for road noise.':
|
||||||
'Ein kurzer Arbeitsweg hilft nur, wenn die Gegend auch zu Budget, Wohnbedarf, Schulwünschen, Sicherheitsgefühl, Breitbandbedarf und Lärmtoleranz passt.',
|
'Ein kurzer Arbeitsweg hilft nur, wenn die Gegend auch zu Budget, Wohnbedarf, Schulwünschen, Sicherheitsgefühl, Breitbandbedarf und Lärmtoleranz passt.',
|
||||||
'How travel-time filters should be interpreted': 'Wie Fahrzeitfilter zu verstehen sind',
|
'How travel-time filters should be interpreted': 'Wie Fahrzeitfilter zu verstehen sind',
|
||||||
|
|
@ -320,8 +316,7 @@ const de: Translations = {
|
||||||
'Die Schulqualität ist ein Teil der engeren Auswahl',
|
'Die Schulqualität ist ein Teil der engeren Auswahl',
|
||||||
'Perfect Postcode helps you compare nearby school data with the other practical constraints that shape a family move: space, price, commute, parks, safety, and local services.':
|
'Perfect Postcode helps you compare nearby school data with the other practical constraints that shape a family move: space, price, commute, parks, safety, and local services.':
|
||||||
'Mit Perfect Postcode vergleichst du Schuldaten in der Nähe mit den anderen praktischen Faktoren eines Familienumzugs: Platz, Preis, Pendelweg, Parks, Sicherheit und lokale Infrastruktur.',
|
'Mit Perfect Postcode vergleichst du Schuldaten in der Nähe mit den anderen praktischen Faktoren eines Familienumzugs: Platz, Preis, Pendelweg, Parks, Sicherheit und lokale Infrastruktur.',
|
||||||
'Check catchments before making decisions':
|
'Check catchments before making decisions': 'Catchments vor Entscheidungen prüfen',
|
||||||
'Catchments vor Entscheidungen prüfen',
|
|
||||||
'Admissions rules and catchment boundaries can change. Use postcode-level school data to find promising areas, then verify current admissions details with the school or local authority.':
|
'Admissions rules and catchment boundaries can change. Use postcode-level school data to find promising areas, then verify current admissions details with the school or local authority.':
|
||||||
'Admissions-Regeln und Catchment-Grenzen können sich ändern. Nutze Schuldaten auf Postcode-Ebene, um vielversprechende Gebiete zu finden, und prüfe aktuelle Details bei Schule oder Local Authority.',
|
'Admissions-Regeln und Catchment-Grenzen können sich ändern. Nutze Schuldaten auf Postcode-Ebene, um vielversprechende Gebiete zu finden, und prüfe aktuelle Details bei Schule oder Local Authority.',
|
||||||
'How to treat school filters': 'Wie du Schulfilter einordnest',
|
'How to treat school filters': 'Wie du Schulfilter einordnest',
|
||||||
|
|
@ -474,12 +469,10 @@ const de: Translations = {
|
||||||
'Make commute constraints explicit': 'Mache Pendelanforderungen klar',
|
'Make commute constraints explicit': 'Mache Pendelanforderungen klar',
|
||||||
'If access to the centre, a station, hospital, university, or business park matters, use travel-time filters first and then compare the remaining postcodes by property data.':
|
'If access to the centre, a station, hospital, university, or business park matters, use travel-time filters first and then compare the remaining postcodes by property data.':
|
||||||
'Wenn die Erreichbarkeit des Zentrums, eines Bahnhofs, Krankenhauses, einer Universität oder eines Business Parks wichtig ist, nutze zuerst Fahrzeitfilter und vergleiche dann die übrigen Postcodes anhand der Immobiliendaten.',
|
'Wenn die Erreichbarkeit des Zentrums, eines Bahnhofs, Krankenhauses, einer Universität oder eines Business Parks wichtig ist, nutze zuerst Fahrzeitfilter und vergleiche dann die übrigen Postcodes anhand der Immobiliendaten.',
|
||||||
'Compare value, not just headline price':
|
'Compare value, not just headline price': 'Wert vergleichen, nicht nur den Angebotspreis',
|
||||||
'Wert vergleichen, nicht nur den Angebotspreis',
|
|
||||||
'Use price, property type, and floor-area filters together. This helps distinguish lower-cost areas from areas that simply contain smaller or different homes.':
|
'Use price, property type, and floor-area filters together. This helps distinguish lower-cost areas from areas that simply contain smaller or different homes.':
|
||||||
'Nutze Preis-, Immobilientyp- und Wohnflächenfilter gemeinsam. So kannst du günstigere Gebiete von Gebieten unterscheiden, in denen einfach kleinere oder andere Immobilien stehen.',
|
'Nutze Preis-, Immobilientyp- und Wohnflächenfilter gemeinsam. So kannst du günstigere Gebiete von Gebieten unterscheiden, in denen einfach kleinere oder andere Immobilien stehen.',
|
||||||
'Screen environmental and local-service signals':
|
'Screen environmental and local-service signals': 'Umwelt- und Infrastruktur-Signale prüfen',
|
||||||
'Umwelt- und Infrastruktur-Signale prüfen',
|
|
||||||
'Road noise, parks, broadband, crime, and amenities can affect whether a property works day to day. Use them as screening criteria before booking viewings.':
|
'Road noise, parks, broadband, crime, and amenities can affect whether a property works day to day. Use them as screening criteria before booking viewings.':
|
||||||
'Straßenlärm, Parks, Breitband, Kriminalität und Infrastruktur können entscheiden, ob eine Immobilie im Alltag funktioniert. Nutze sie als Screening-Kriterien, bevor du Besichtigungen buchst.',
|
'Straßenlärm, Parks, Breitband, Kriminalität und Infrastruktur können entscheiden, ob eine Immobilie im Alltag funktioniert. Nutze sie als Screening-Kriterien, bevor du Besichtigungen buchst.',
|
||||||
'Can I use this for commuter villages around Bristol?':
|
'Can I use this for commuter villages around Bristol?':
|
||||||
|
|
@ -604,8 +597,7 @@ const de: Translations = {
|
||||||
logIn: 'Anmelden',
|
logIn: 'Anmelden',
|
||||||
createAccount: 'Konto erstellen',
|
createAccount: 'Konto erstellen',
|
||||||
resetPassword: 'Passwort zurücksetzen',
|
resetPassword: 'Passwort zurücksetzen',
|
||||||
valueProp:
|
valueProp: 'Speichere Suchen, merke Immobilien und erstelle eine Shortlist passender Gebiete.',
|
||||||
'Speichere Suchen, merke Immobilien und erstelle eine Shortlist passender Gebiete.',
|
|
||||||
continueWithGoogle: 'Weiter mit Google',
|
continueWithGoogle: 'Weiter mit Google',
|
||||||
email: 'E-Mail',
|
email: 'E-Mail',
|
||||||
emailPlaceholder: 'name@beispiel.de',
|
emailPlaceholder: 'name@beispiel.de',
|
||||||
|
|
|
||||||
|
|
@ -322,8 +322,7 @@ const fr: Translations = {
|
||||||
'La qualité des écoles n’est qu’une partie de la sélection',
|
'La qualité des écoles n’est qu’une partie de la sélection',
|
||||||
'Perfect Postcode helps you compare nearby school data with the other practical constraints that shape a family move: space, price, commute, parks, safety, and local services.':
|
'Perfect Postcode helps you compare nearby school data with the other practical constraints that shape a family move: space, price, commute, parks, safety, and local services.':
|
||||||
'Perfect Postcode vous aide à comparer les données des écoles à proximité avec les autres contraintes pratiques d’un déménagement familial : espace, prix, trajet, parcs, sécurité et services locaux.',
|
'Perfect Postcode vous aide à comparer les données des écoles à proximité avec les autres contraintes pratiques d’un déménagement familial : espace, prix, trajet, parcs, sécurité et services locaux.',
|
||||||
'Check catchments before making decisions':
|
'Check catchments before making decisions': 'Vérifier les catchment areas avant de décider',
|
||||||
'Vérifier les catchment areas avant de décider',
|
|
||||||
'Admissions rules and catchment boundaries can change. Use postcode-level school data to find promising areas, then verify current admissions details with the school or local authority.':
|
'Admissions rules and catchment boundaries can change. Use postcode-level school data to find promising areas, then verify current admissions details with the school or local authority.':
|
||||||
'Les règles d’admission et les limites de catchment peuvent changer. Utilisez les données scolaires par code postal pour repérer des secteurs prometteurs, puis vérifiez les admissions à jour auprès de l’école ou de l’autorité locale.',
|
'Les règles d’admission et les limites de catchment peuvent changer. Utilisez les données scolaires par code postal pour repérer des secteurs prometteurs, puis vérifiez les admissions à jour auprès de l’école ou de l’autorité locale.',
|
||||||
'How to treat school filters': 'Comment traiter les filtres scolaires',
|
'How to treat school filters': 'Comment traiter les filtres scolaires',
|
||||||
|
|
@ -382,8 +381,7 @@ const fr: Translations = {
|
||||||
'Ce qu’une vérification du code postal ne peut pas prouver',
|
'Ce qu’une vérification du code postal ne peut pas prouver',
|
||||||
'It can’t confirm the condition of a home, future development, legal title, lender requirements, or current street-level experience. Those still need direct checks.':
|
'It can’t confirm the condition of a home, future development, legal title, lender requirements, or current street-level experience. Those still need direct checks.':
|
||||||
'Il ne peut pas confirmer l’état d’un logement, les futurs projets, le titre de propriété, les exigences du prêteur ou le ressenti actuel dans la rue. Ces points demandent encore des vérifications directes.',
|
'Il ne peut pas confirmer l’état d’un logement, les futurs projets, le titre de propriété, les exigences du prêteur ou le ressenti actuel dans la rue. Ces points demandent encore des vérifications directes.',
|
||||||
'Can I use the checker before a viewing?':
|
'Can I use the checker before a viewing?': 'Puis-je utiliser cet outil avant une visite ?',
|
||||||
'Puis-je utiliser cet outil avant une visite ?',
|
|
||||||
'Yes. That’s one of the main use cases: screen the postcode first, then decide whether the viewing is worth the time.':
|
'Yes. That’s one of the main use cases: screen the postcode first, then decide whether the viewing is worth the time.':
|
||||||
'Oui. C’est l’un des principaux cas d’utilisation : examinez d’abord le code postal, puis décidez si la visite vaut le temps consacré.',
|
'Oui. C’est l’un des principaux cas d’utilisation : examinez d’abord le code postal, puis décidez si la visite vaut le temps consacré.',
|
||||||
'Does the checker include exact property condition?':
|
'Does the checker include exact property condition?':
|
||||||
|
|
@ -482,8 +480,7 @@ const fr: Translations = {
|
||||||
'Compare value, not just headline price': 'Comparez la valeur, pas seulement le prix affiché',
|
'Compare value, not just headline price': 'Comparez la valeur, pas seulement le prix affiché',
|
||||||
'Use price, property type, and floor-area filters together. This helps distinguish lower-cost areas from areas that simply contain smaller or different homes.':
|
'Use price, property type, and floor-area filters together. This helps distinguish lower-cost areas from areas that simply contain smaller or different homes.':
|
||||||
'Utilisez ensemble les filtres de prix, de type de bien et de surface. Cela distingue les secteurs vraiment moins chers de ceux qui comptent simplement des logements plus petits ou différents.',
|
'Utilisez ensemble les filtres de prix, de type de bien et de surface. Cela distingue les secteurs vraiment moins chers de ceux qui comptent simplement des logements plus petits ou différents.',
|
||||||
'Screen environmental and local-service signals':
|
'Screen environmental and local-service signals': 'Examiner environnement et services locaux',
|
||||||
'Examiner environnement et services locaux',
|
|
||||||
'Road noise, parks, broadband, crime, and amenities can affect whether a property works day to day. Use them as screening criteria before booking viewings.':
|
'Road noise, parks, broadband, crime, and amenities can affect whether a property works day to day. Use them as screening criteria before booking viewings.':
|
||||||
'Le bruit routier, les parcs, le haut débit, la criminalité et les services de proximité peuvent affecter la vie quotidienne dans un logement. Utilisez-les comme critères de tri avant de réserver des visites.',
|
'Le bruit routier, les parcs, le haut débit, la criminalité et les services de proximité peuvent affecter la vie quotidienne dans un logement. Utilisez-les comme critères de tri avant de réserver des visites.',
|
||||||
'Can I use this for commuter villages around Bristol?':
|
'Can I use this for commuter villages around Bristol?':
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,8 @@ const hi: Translations = {
|
||||||
'Relax one constraint at a time': 'एक बार में एक constraint ढीला करें',
|
'Relax one constraint at a time': 'एक बार में एक constraint ढीला करें',
|
||||||
'When the search becomes too narrow, loosen a single filter and watch which postcodes reappear. This makes compromise explicit instead of relying on guesswork.':
|
'When the search becomes too narrow, loosen a single filter and watch which postcodes reappear. This makes compromise explicit instead of relying on guesswork.':
|
||||||
'जब search बहुत संकरी हो जाए, तो एक फ़िल्टर ढीला करें और देखें कौन से पोस्टकोड वापस आते हैं. इससे guesswork के बजाय compromise साफ दिखता है.',
|
'जब search बहुत संकरी हो जाए, तो एक फ़िल्टर ढीला करें और देखें कौन से पोस्टकोड वापस आते हैं. इससे guesswork के बजाय compromise साफ दिखता है.',
|
||||||
'Turn vague areas into specific postcodes': 'धुंधले area ideas को specific postcodes में बदलें',
|
'Turn vague areas into specific postcodes':
|
||||||
|
'धुंधले area ideas को specific postcodes में बदलें',
|
||||||
'Broad town or borough searches hide large differences between streets. Perfect Postcode helps you move from a general area to postcodes that satisfy your hard requirements.':
|
'Broad town or borough searches hide large differences between streets. Perfect Postcode helps you move from a general area to postcodes that satisfy your hard requirements.':
|
||||||
'Town या borough-level searches सड़कों के बीच बड़े फर्क छिपा देती हैं. Perfect Postcode आपको general area से उन पोस्टकोड तक ले जाता है जो आपकी hard requirements पूरी करते हैं.',
|
'Town या borough-level searches सड़कों के बीच बड़े फर्क छिपा देती हैं. Perfect Postcode आपको general area से उन पोस्टकोड तक ले जाता है जो आपकी hard requirements पूरी करते हैं.',
|
||||||
'Keep trade-offs visible': 'Trade-offs साफ रखें',
|
'Keep trade-offs visible': 'Trade-offs साफ रखें',
|
||||||
|
|
@ -217,7 +218,8 @@ const hi: Translations = {
|
||||||
'How to use the results': 'परिणामों का उपयोग कैसे करें',
|
'How to use the results': 'परिणामों का उपयोग कैसे करें',
|
||||||
'Treat matching postcodes as a research queue: check live listings, visit streets, confirm schools and admissions, and review current official sources.':
|
'Treat matching postcodes as a research queue: check live listings, visit streets, confirm schools and admissions, and review current official sources.':
|
||||||
'Matching postcodes को research queue की तरह लें: live listings देखें, streets visit करें, schools और admissions confirm करें, और current official sources check करें.',
|
'Matching postcodes को research queue की तरह लें: live listings देखें, streets visit करें, schools और admissions confirm करें, और current official sources check करें.',
|
||||||
'Can I save a postcode property search?': 'क्या मैं postcode property search सेव कर सकता हूँ?',
|
'Can I save a postcode property search?':
|
||||||
|
'क्या मैं postcode property search सेव कर सकता हूँ?',
|
||||||
'Yes. Licensed users can save searches and return to them later. Saved searches are designed for shortlists and comparison notes.':
|
'Yes. Licensed users can save searches and return to them later. Saved searches are designed for shortlists and comparison notes.':
|
||||||
'हाँ. Licensed users searches सेव कर सकते हैं और बाद में वहीं लौट सकते हैं. Saved searches shortlist और comparison notes के लिए बनाई गई हैं.',
|
'हाँ. Licensed users searches सेव कर सकते हैं और बाद में वहीं लौट सकते हैं. Saved searches shortlist और comparison notes के लिए बनाई गई हैं.',
|
||||||
'Can I search without knowing the area?': 'क्या area जाने बिना search कर सकता हूँ?',
|
'Can I search without knowing the area?': 'क्या area जाने बिना search कर सकता हूँ?',
|
||||||
|
|
@ -231,8 +233,7 @@ const hi: Translations = {
|
||||||
'Greater Manchester के आसपास broad search को narrow करने के लिए regional guide.',
|
'Greater Manchester के आसपास broad search को narrow करने के लिए regional guide.',
|
||||||
'Start a postcode search': 'Postcode search शुरू करें',
|
'Start a postcode search': 'Postcode search शुरू करें',
|
||||||
'Commute property search': 'Commute के हिसाब से प्रॉपर्टी खोजें',
|
'Commute property search': 'Commute के हिसाब से प्रॉपर्टी खोजें',
|
||||||
'Search for places to live by commute time':
|
'Search for places to live by commute time': 'Commute time के हिसाब से रहने की जगहें खोजें',
|
||||||
'Commute time के हिसाब से रहने की जगहें खोजें',
|
|
||||||
'Commute property search - Find places to live by travel time':
|
'Commute property search - Find places to live by travel time':
|
||||||
'Commute property search - travel time से रहने की जगहें खोजें',
|
'Commute property search - travel time से रहने की जगहें खोजें',
|
||||||
'Filter postcodes by commute time, then compare price, schools, safety, broadband, road noise, parks and property data on one map.':
|
'Filter postcodes by commute time, then compare price, schools, safety, broadband, road noise, parks and property data on one map.':
|
||||||
|
|
@ -260,8 +261,7 @@ const hi: Translations = {
|
||||||
'Journey time को move की बाकी जरूरतों से balance करें',
|
'Journey time को move की बाकी जरूरतों से balance करें',
|
||||||
'A fast commute only helps if the area also fits your budget, housing needs, school preferences, safety threshold, broadband requirement, and tolerance for road noise.':
|
'A fast commute only helps if the area also fits your budget, housing needs, school preferences, safety threshold, broadband requirement, and tolerance for road noise.':
|
||||||
'Fast commute तभी मदद करता है जब area आपके बजट, housing needs, school preferences, safety threshold, ब्रॉडबैंड जरूरत और road noise tolerance से भी match करता हो.',
|
'Fast commute तभी मदद करता है जब area आपके बजट, housing needs, school preferences, safety threshold, ब्रॉडबैंड जरूरत और road noise tolerance से भी match करता हो.',
|
||||||
'How travel-time filters should be interpreted':
|
'How travel-time filters should be interpreted': 'Travel-time filters को कैसे पढ़ें',
|
||||||
'Travel-time filters को कैसे पढ़ें',
|
|
||||||
'Travel-time modelling is useful for comparing areas consistently. Before committing, check current timetables, disruption patterns, parking, cycling conditions, and walking routes.':
|
'Travel-time modelling is useful for comparing areas consistently. Before committing, check current timetables, disruption patterns, parking, cycling conditions, and walking routes.':
|
||||||
'Travel-time modelling इलाकों की consistent comparison के लिए useful है. Commit करने से पहले current timetables, disruption patterns, parking, cycling conditions और walking routes जरूर check करें.',
|
'Travel-time modelling इलाकों की consistent comparison के लिए useful है. Commit करने से पहले current timetables, disruption patterns, parking, cycling conditions और walking routes जरूर check करें.',
|
||||||
'Why commute filters are combined with property data':
|
'Why commute filters are combined with property data':
|
||||||
|
|
@ -305,7 +305,8 @@ const hi: Translations = {
|
||||||
'Verify admissions before deciding': 'फैसले से पहले admissions verify करें',
|
'Verify admissions before deciding': 'फैसले से पहले admissions verify करें',
|
||||||
'School data can point to promising areas, but admissions rules and catchments can change. Confirm current arrangements with schools and local authorities.':
|
'School data can point to promising areas, but admissions rules and catchments can change. Confirm current arrangements with schools and local authorities.':
|
||||||
'School data promising areas दिखा सकता है, लेकिन admissions rules और catchments बदल सकते हैं. Schools और local authorities से current arrangements confirm करें.',
|
'School data promising areas दिखा सकता है, लेकिन admissions rules और catchments बदल सकते हैं. Schools और local authorities से current arrangements confirm करें.',
|
||||||
'School quality is one part of the shortlist': 'School quality shortlist का सिर्फ एक हिस्सा है',
|
'School quality is one part of the shortlist':
|
||||||
|
'School quality shortlist का सिर्फ एक हिस्सा है',
|
||||||
'Perfect Postcode helps you compare nearby school data with the other practical constraints that shape a family move: space, price, commute, parks, safety, and local services.':
|
'Perfect Postcode helps you compare nearby school data with the other practical constraints that shape a family move: space, price, commute, parks, safety, and local services.':
|
||||||
'Perfect Postcode nearby school data को उन practical constraints के साथ compare करने में मदद करता है जो family move को shape करती हैं: space, price, commute, parks, safety और local services.',
|
'Perfect Postcode nearby school data को उन practical constraints के साथ compare करने में मदद करता है जो family move को shape करती हैं: space, price, commute, parks, safety और local services.',
|
||||||
'Check catchments before making decisions': 'फैसले से पहले catchments check करें',
|
'Check catchments before making decisions': 'फैसले से पहले catchments check करें',
|
||||||
|
|
@ -317,7 +318,8 @@ const hi: Translations = {
|
||||||
'Family trade-offs to compare': 'Compare करने लायक family trade-offs',
|
'Family trade-offs to compare': 'Compare करने लायक family trade-offs',
|
||||||
'Combine schools with parks, road noise, crime, property size, commute, broadband, and price so the shortlist reflects the whole move.':
|
'Combine schools with parks, road noise, crime, property size, commute, broadband, and price so the shortlist reflects the whole move.':
|
||||||
'Schools को parks, road noise, crime, property size, commute, broadband और price के साथ मिलाएं ताकि shortlist पूरा move दिखाए.',
|
'Schools को parks, road noise, crime, property size, commute, broadband और price के साथ मिलाएं ताकि shortlist पूरा move दिखाए.',
|
||||||
'Does this show school catchment guarantees?': 'क्या यह school catchment guarantee दिखाता है?',
|
'Does this show school catchment guarantees?':
|
||||||
|
'क्या यह school catchment guarantee दिखाता है?',
|
||||||
'No. It helps identify promising areas, but catchments and admissions must be verified with the school or local authority.':
|
'No. It helps identify promising areas, but catchments and admissions must be verified with the school or local authority.':
|
||||||
'नहीं. यह promising areas पहचानने में मदद करता है, लेकिन catchments और admissions school या local authority से verify करने होंगे.',
|
'नहीं. यह promising areas पहचानने में मदद करता है, लेकिन catchments और admissions school या local authority से verify करने होंगे.',
|
||||||
'Can I combine school filters with parks and safety?':
|
'Can I combine school filters with parks and safety?':
|
||||||
|
|
@ -396,8 +398,7 @@ const hi: Translations = {
|
||||||
'Compare price with property type': 'Price को property type के साथ compare करें',
|
'Compare price with property type': 'Price को property type के साथ compare करें',
|
||||||
'Median prices alone can be misleading if the local property mix changes. Add property type, tenure, floor area, and price filters so similar areas are compared fairly.':
|
'Median prices alone can be misleading if the local property mix changes. Add property type, tenure, floor area, and price filters so similar areas are compared fairly.':
|
||||||
'अगर local property mix बदलता है, तो सिर्फ median prices misleading हो सकती हैं. Similar areas की fair comparison के लिए property type, tenure, floor area और price filters जोड़ें.',
|
'अगर local property mix बदलता है, तो सिर्फ median prices misleading हो सकती हैं. Similar areas की fair comparison के लिए property type, tenure, floor area और price filters जोड़ें.',
|
||||||
'Keep family and environment trade-offs visible':
|
'Keep family and environment trade-offs visible': 'Family और environment trade-offs साफ रखें',
|
||||||
'Family और environment trade-offs साफ रखें',
|
|
||||||
'Layer school context, parks, road noise, broadband, and crime signals on top of the property filters. That makes it easier to decide which compromises are acceptable.':
|
'Layer school context, parks, road noise, broadband, and crime signals on top of the property filters. That makes it easier to decide which compromises are acceptable.':
|
||||||
'Property filters के ऊपर school context, parks, road noise, broadband और crime signals layer करें. इससे तय करना आसान होता है कि कौन से compromises acceptable हैं.',
|
'Property filters के ऊपर school context, parks, road noise, broadband और crime signals layer करें. इससे तय करना आसान होता है कि कौन से compromises acceptable हैं.',
|
||||||
'Can Perfect Postcode tell me the best area in Birmingham?':
|
'Can Perfect Postcode tell me the best area in Birmingham?':
|
||||||
|
|
@ -460,8 +461,7 @@ const hi: Translations = {
|
||||||
'Make commute constraints explicit': 'Commute constraints साफ करें',
|
'Make commute constraints explicit': 'Commute constraints साफ करें',
|
||||||
'If access to the centre, a station, hospital, university, or business park matters, use travel-time filters first and then compare the remaining postcodes by property data.':
|
'If access to the centre, a station, hospital, university, or business park matters, use travel-time filters first and then compare the remaining postcodes by property data.':
|
||||||
'अगर centre, station, hospital, university या business park तक access मायने रखती है, तो पहले travel-time filters लगाएं और फिर बचे postcodes को property data से compare करें.',
|
'अगर centre, station, hospital, university या business park तक access मायने रखती है, तो पहले travel-time filters लगाएं और फिर बचे postcodes को property data से compare करें.',
|
||||||
'Compare value, not just headline price':
|
'Compare value, not just headline price': 'सिर्फ headline price नहीं, value compare करें',
|
||||||
'सिर्फ headline price नहीं, value compare करें',
|
|
||||||
'Use price, property type, and floor-area filters together. This helps distinguish lower-cost areas from areas that simply contain smaller or different homes.':
|
'Use price, property type, and floor-area filters together. This helps distinguish lower-cost areas from areas that simply contain smaller or different homes.':
|
||||||
'Price, property type और floor-area filters साथ इस्तेमाल करें. इससे सच में lower-cost areas को उन areas से अलग करना आसान होता है जहां बस छोटे या अलग homes हैं.',
|
'Price, property type और floor-area filters साथ इस्तेमाल करें. इससे सच में lower-cost areas को उन areas से अलग करना आसान होता है जहां बस छोटे या अलग homes हैं.',
|
||||||
'Screen environmental and local-service signals':
|
'Screen environmental and local-service signals':
|
||||||
|
|
|
||||||
|
|
@ -471,8 +471,7 @@ const hu: Translations = {
|
||||||
'Make commute constraints explicit': 'Tedd egyértelművé az ingázási korlátokat',
|
'Make commute constraints explicit': 'Tedd egyértelművé az ingázási korlátokat',
|
||||||
'If access to the centre, a station, hospital, university, or business park matters, use travel-time filters first and then compare the remaining postcodes by property data.':
|
'If access to the centre, a station, hospital, university, or business park matters, use travel-time filters first and then compare the remaining postcodes by property data.':
|
||||||
'Ha fontos a központ, állomás, kórház, egyetem vagy business park elérése, először használd az utazási idő szűrőit, majd hasonlítsd össze a fennmaradó irányítószámokat ingatlanadatok alapján.',
|
'Ha fontos a központ, állomás, kórház, egyetem vagy business park elérése, először használd az utazási idő szűrőit, majd hasonlítsd össze a fennmaradó irányítószámokat ingatlanadatok alapján.',
|
||||||
'Compare value, not just headline price':
|
'Compare value, not just headline price': 'Az értéket nézd, ne csak a kiemelt árat',
|
||||||
'Az értéket nézd, ne csak a kiemelt árat',
|
|
||||||
'Use price, property type, and floor-area filters together. This helps distinguish lower-cost areas from areas that simply contain smaller or different homes.':
|
'Use price, property type, and floor-area filters together. This helps distinguish lower-cost areas from areas that simply contain smaller or different homes.':
|
||||||
'Használd együtt az ár-, ingatlantípus- és alapterület-szűrőket. Ez segít megkülönböztetni az olcsóbb területeket azoktól, ahol egyszerűen kisebb vagy eltérő otthonok vannak.',
|
'Használd együtt az ár-, ingatlantípus- és alapterület-szűrőket. Ez segít megkülönböztetni az olcsóbb területeket azoktól, ahol egyszerűen kisebb vagy eltérő otthonok vannak.',
|
||||||
'Screen environmental and local-service signals':
|
'Screen environmental and local-service signals':
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,8 @@ const zh: Translations = {
|
||||||
'按邮编筛选历史成交价与当前估值。',
|
'按邮编筛选历史成交价与当前估值。',
|
||||||
'Compare value with commute, schools, broadband, crime, noise, and amenities.':
|
'Compare value with commute, schools, broadband, crime, noise, and amenities.':
|
||||||
'把房价与通勤、学校、宽带、治安、噪音和周边设施放在一起比较。',
|
'把房价与通勤、学校、宽带、治安、噪音和周边设施放在一起比较。',
|
||||||
'Build a shortlist before spending weekends on viewings.': '把周末花在看房前,先整理出候选名单。',
|
'Build a shortlist before spending weekends on viewings.':
|
||||||
|
'把周末花在看房前,先整理出候选名单。',
|
||||||
'Find postcodes that fit the budget before listings appear':
|
'Find postcodes that fit the budget before listings appear':
|
||||||
'抢在房源上架之前,先锁定符合预算的邮编',
|
'抢在房源上架之前,先锁定符合预算的邮编',
|
||||||
'Start with a maximum price and property type, then colour the map by price per square metre or estimated current price. This helps reveal areas where similar homes have historically traded within reach, even when there are no live listings today.':
|
'Start with a maximum price and property type, then colour the map by price per square metre or estimated current price. This helps reveal areas where similar homes have historically traded within reach, even when there are no live listings today.':
|
||||||
|
|
@ -1074,7 +1075,8 @@ const zh: Translations = {
|
||||||
dsConservationAreasUse: '英格兰指定保护区边界。用于标记邮编代表点是否位于保护区内。',
|
dsConservationAreasUse: '英格兰指定保护区边界。用于标记邮编代表点是否位于保护区内。',
|
||||||
dsListedBuildingsName: 'Historic England 受保护建筑',
|
dsListedBuildingsName: 'Historic England 受保护建筑',
|
||||||
dsListedBuildingsOrigin: 'Historic England 英格兰国家遗产名录',
|
dsListedBuildingsOrigin: 'Historic England 英格兰国家遗产名录',
|
||||||
dsListedBuildingsUse: '英格兰受保护建筑点位记录。用于标记地址似乎与附近受保护建筑条目匹配的房产。',
|
dsListedBuildingsUse:
|
||||||
|
'英格兰受保护建筑点位记录。用于标记地址似乎与附近受保护建筑条目匹配的房产。',
|
||||||
dsNaptanName: 'NaPTAN(公共交通站点)',
|
dsNaptanName: 'NaPTAN(公共交通站点)',
|
||||||
dsNaptanOrigin: 'Department for Transport',
|
dsNaptanOrigin: 'Department for Transport',
|
||||||
dsNaptanUse: '英格兰各地铁路、公交、地铁/有轨电车、渡轮和机场的站点位置。',
|
dsNaptanUse: '英格兰各地铁路、公交、地铁/有轨电车、渡轮和机场的站点位置。',
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,6 @@ def _download_and_extract(
|
||||||
"""Download one survey zip and extract its ECW raster(s)."""
|
"""Download one survey zip and extract its ECW raster(s)."""
|
||||||
url = f"{tile.uri}?subscription-key={key}"
|
url = f"{tile.uri}?subscription-key={key}"
|
||||||
zip_path = ecw_dir / f"{tile.os_tile_id}.zip"
|
zip_path = ecw_dir / f"{tile.os_tile_id}.zip"
|
||||||
last_error: Exception | None = None
|
|
||||||
for attempt in range(retries + 1):
|
for attempt in range(retries + 1):
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(
|
with urllib.request.urlopen(
|
||||||
|
|
@ -196,7 +195,6 @@ def _download_and_extract(
|
||||||
shutil.copyfileobj(response, out, length=1 << 20)
|
shutil.copyfileobj(response, out, length=1 << 20)
|
||||||
break
|
break
|
||||||
except (urllib.error.URLError, TimeoutError, ConnectionError) as err:
|
except (urllib.error.URLError, TimeoutError, ConnectionError) as err:
|
||||||
last_error = err
|
|
||||||
if attempt == retries:
|
if attempt == retries:
|
||||||
raise RuntimeError(f"Failed to download {url}: {err}") from err
|
raise RuntimeError(f"Failed to download {url}: {err}") from err
|
||||||
extracted: list[Path] = []
|
extracted: list[Path] = []
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,11 @@ Pre-allocates numpy arrays at 25M capacity and grows by 1.5x if needed (using in
|
||||||
|
|
||||||
**Loading** (`inspire.py:load_inspire`): Bboxes and offsets are loaded into RAM (~1.1GB). Coords are memory-mapped — the OS pages them in on demand from the ~3GB file, never loading the whole thing.
|
**Loading** (`inspire.py:load_inspire`): Bboxes and offsets are loaded into RAM (~1.1GB). Coords are memory-mapped — the OS pages them in on demand from the ~3GB file, never loading the whole thing.
|
||||||
|
|
||||||
**Candidate retrieval** (`inspire.py:get_inspire_candidates`): Given an OA's bounding box, performs a vectorized numpy overlap test against all 24M INSPIRE bboxes — four comparisons broadcast across the entire array. Typically matches 10-500 parcels per OA. Only those matches are materialized as Shapely Polygon objects by reading their coordinate slice from the memory-mapped file. Invalid polygons are repaired with `make_valid`.
|
**Candidate retrieval** (`inspire.py:InspireIndex`): A uniform 1km grid index is built once over the 24M parcel bboxes (`build_inspire_index`). Each OA lookup (`InspireIndex.candidates`) gathers parcels from the cells its bounding box covers plus a small overflow list of parcels larger than one cell, then applies the exact bbox overlap test — O(cells + candidates) instead of an O(24M) scan per OA (the old linear scan was ~4h of the run on its own). The candidate set and order are identical to the scan. Typically matches 10-500 parcels per OA, materialized as Shapely Polygon objects by reading their coordinate slice from the memory-mapped file; invalid polygons are repaired with `make_valid`.
|
||||||
|
|
||||||
### Phase 3: Processing OAs
|
### Phase 3: Processing OAs
|
||||||
|
|
||||||
The main loop in `__main__.py` iterates through every OA that has both a boundary polygon and UPRNs. For each OA, it retrieves the OA's UPRN points and postcodes.
|
The main loop in `__main__.py` (`_process_oas`) iterates through every OA that has both a boundary polygon and UPRNs. For each OA, it retrieves the OA's UPRN points and postcodes. OAs are independent, so the loop fans out across CPU cores with a `fork` process pool (`--workers`, default all CPUs): workers share the big read-only inputs (INSPIRE arrays + coords mmap, UPRN arrays, OA geometries) copy-on-write and return WKB-encoded fragments. Workers slice the UPRN data from plain numpy/Arrow arrays (`extract_uprn_arrays`) rather than polars, avoiding the fork-after-threads hazard of polars' thread pool. Fragment order doesn't affect the output (`merge_fragments` unions per postcode), so the parallel result is identical to single-process.
|
||||||
|
|
||||||
**Fast path**: If every UPRN in the OA shares the same postcode, the entire OA polygon is assigned to that postcode. No geometry computation needed. This covers the majority of OAs (~70-80%).
|
**Fast path**: If every UPRN in the OA shares the same postcode, the entire OA polygon is assigned to that postcode. No geometry computation needed. This covers the majority of OAs (~70-80%).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ struct ParsedPostcodeList {
|
||||||
fn parse_postcode_list(
|
fn parse_postcode_list(
|
||||||
raw: &str,
|
raw: &str,
|
||||||
state: &crate::state::AppState,
|
state: &crate::state::AppState,
|
||||||
) -> Result<ParsedPostcodeList, axum::response::Response> {
|
) -> Result<ParsedPostcodeList, (StatusCode, String)> {
|
||||||
let mut entries: Vec<(usize, String)> = Vec::new();
|
let mut entries: Vec<(usize, String)> = Vec::new();
|
||||||
let mut unknown: Vec<String> = Vec::new();
|
let mut unknown: Vec<String> = Vec::new();
|
||||||
let mut seen: FxHashSet<usize> = FxHashSet::default();
|
let mut seen: FxHashSet<usize> = FxHashSet::default();
|
||||||
|
|
@ -229,8 +229,7 @@ fn parse_postcode_list(
|
||||||
"Too many postcodes; at most {} are supported per export",
|
"Too many postcodes; at most {} are supported per export",
|
||||||
MAX_EXPORT_POSTCODES
|
MAX_EXPORT_POSTCODES
|
||||||
),
|
),
|
||||||
)
|
));
|
||||||
.into_response());
|
|
||||||
}
|
}
|
||||||
match state.postcode_data.postcode_to_idx.get(&normalized) {
|
match state.postcode_data.postcode_to_idx.get(&normalized) {
|
||||||
Some(&pc_idx) if seen.insert(pc_idx) => {
|
Some(&pc_idx) if seen.insert(pc_idx) => {
|
||||||
|
|
@ -245,8 +244,7 @@ fn parse_postcode_list(
|
||||||
return Err((
|
return Err((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"No valid postcodes supplied".to_string(),
|
"No valid postcodes supplied".to_string(),
|
||||||
)
|
));
|
||||||
.into_response());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ParsedPostcodeList { entries, unknown })
|
Ok(ParsedPostcodeList { entries, unknown })
|
||||||
|
|
@ -296,7 +294,9 @@ pub async fn get_export(
|
||||||
|
|
||||||
// Two modes: bounds-based (default) and explicit postcode list.
|
// Two modes: bounds-based (default) and explicit postcode list.
|
||||||
let postcode_list = match params.postcodes.as_deref() {
|
let postcode_list = match params.postcodes.as_deref() {
|
||||||
Some(raw) if !raw.trim().is_empty() => Some(parse_postcode_list(raw, &state)?),
|
Some(raw) if !raw.trim().is_empty() => {
|
||||||
|
Some(parse_postcode_list(raw, &state).map_err(IntoResponse::into_response)?)
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let is_postcode_mode = postcode_list.is_some();
|
let is_postcode_mode = postcode_list.is_some();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue