This commit is contained in:
Andras Schmelczer 2026-05-08 09:27:54 +01:00
parent a9bad79339
commit 701c17a703
14 changed files with 3243 additions and 2974 deletions

View file

@ -189,8 +189,8 @@ $(GREENSPACE): $(PBF)
$(OS_GREENSPACE):
uv run python -m pipeline.download.os_greenspace --output $@
$(PLACES): $(PBF) $(ENGLAND_BOUNDARY)
uv run python -m pipeline.download.places --output $@ --pbf $(PBF) --boundary $(ENGLAND_BOUNDARY)
$(PLACES): $(PBF) $(ENGLAND_BOUNDARY) $(NAPTAN)
uv run python -m pipeline.download.places --output $@ --pbf $(PBF) --boundary $(ENGLAND_BOUNDARY) --naptan $(NAPTAN)
$(LSOA_POP):
uv run python -m pipeline.download.lsoa_population --output $@

View file

@ -0,0 +1,54 @@
const js = require('@eslint/js');
const tsParser = require('@typescript-eslint/parser');
const tsPlugin = require('@typescript-eslint/eslint-plugin');
const globals = require('globals');
const reactPlugin = require('eslint-plugin-react');
const reactHooksPlugin = require('eslint-plugin-react-hooks');
module.exports = [
{
ignores: ['dist/**', 'node_modules/**'],
linterOptions: {
reportUnusedDisableDirectives: false,
},
},
js.configs.recommended,
{
files: ['src/**/*.{ts,tsx}'],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
globals: {
...globals.browser,
...globals.es2021,
},
},
plugins: {
'@typescript-eslint': tsPlugin,
react: reactPlugin,
'react-hooks': reactHooksPlugin,
},
settings: {
react: {
version: 'detect',
},
},
rules: {
...reactPlugin.configs.recommended.rules,
...tsPlugin.configs.recommended.rules,
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'no-undef': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
},
];

File diff suppressed because it is too large Load diff

View file

@ -15,62 +15,68 @@
"check:i18n": "node scripts/check-translations.mjs"
},
"dependencies": {
"@deck.gl/core": "^9.0.0",
"@deck.gl/geo-layers": "^9.0.0",
"@deck.gl/layers": "^9.0.0",
"@deck.gl/mapbox": "^9.2.6",
"@deck.gl/react": "^9.0.0",
"@plausible-analytics/tracker": "^0.4.4",
"@protomaps/basemaps": "^5.7.0",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slider": "^1.1.0",
"@deck.gl/core": "^9.3.2",
"@deck.gl/extensions": "^9.3.2",
"@deck.gl/geo-layers": "^9.3.2",
"@deck.gl/layers": "^9.3.2",
"@deck.gl/mapbox": "^9.3.2",
"@deck.gl/mesh-layers": "^9.3.2",
"@deck.gl/react": "^9.3.2",
"@deck.gl/widgets": "^9.3.2",
"@plausible-analytics/tracker": "^0.4.5",
"@protomaps/basemaps": "^5.7.2",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slider": "^1.3.6",
"@types/supercluster": "^7.1.3",
"i18next": "^26.0.3",
"maplibre-gl": "^4.0.0",
"i18next": "^26.0.10",
"maplibre-gl": "^5.24.0",
"pocketbase": "^0.26.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^17.0.2",
"react-joyride": "^2.9.3",
"react-map-gl": "^7.1.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-i18next": "^17.0.7",
"react-joyride": "^3.1.0",
"react-map-gl": "^8.1.1",
"supercluster": "^8.0.1"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.0",
"@babel/preset-env": "^7.29.5",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@eslint/js": "^9.39.4",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@tailwindcss/postcss": "^4.2.4",
"@testing-library/react": "^16.3.2",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"autoprefixer": "^10.4.0",
"babel-loader": "^10.0.0",
"copy-webpack-plugin": "^13.0.1",
"css-loader": "^7.0.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.0",
"eslint-plugin-react-hooks": "^4.6.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.59.2",
"@typescript-eslint/parser": "^8.59.2",
"autoprefixer": "^10.5.0",
"babel-loader": "^10.1.1",
"copy-webpack-plugin": "^14.0.0",
"css-loader": "^7.1.4",
"eslint": "^9.39.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
"favicons": "^7.2.0",
"favicons-webpack-plugin": "^6.0.1",
"html-webpack-plugin": "^5.6.0",
"globals": "^17.6.0",
"html-webpack-plugin": "^5.6.7",
"jsdom": "^29.1.1",
"mini-css-extract-plugin": "^2.9.0",
"postcss": "^8.4.0",
"postcss-loader": "^8.0.0",
"prettier": "^3.2.0",
"puppeteer": "^24.0.0",
"mini-css-extract-plugin": "^2.10.2",
"postcss": "^8.5.14",
"postcss-loader": "^8.2.1",
"prettier": "^3.8.3",
"puppeteer": "^24.43.0",
"react-refresh": "^0.18.0",
"sharp": "^0.34.5",
"style-loader": "^4.0.0",
"tailwindcss": "^3.4.0",
"ts-loader": "^9.5.0",
"typescript": "^5.4.0",
"tailwindcss": "^4.2.4",
"ts-loader": "^9.5.7",
"typescript": "^6.0.3",
"vitest": "^4.1.5",
"webpack": "^5.90.0",
"webpack-cli": "^5.1.0",
"webpack-dev-server": "^5.0.0"
"webpack": "^5.106.2",
"webpack-cli": "^7.0.2",
"webpack-dev-server": "^5.2.3"
}
}

View file

@ -1,6 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
'@tailwindcss/postcss': {},
autoprefixer: {},
},
};

1
frontend/src/global.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module '*.css';

View file

@ -261,12 +261,13 @@ const de: Translations = {
// ── Area Pane ──────────────────────────────────────
areaPane: {
areaStatistics: 'Gebietsstatistiken',
areaOverview: 'Übersicht',
statsFor: 'Statistiken für alle Immobilien in diesem {{type}}',
matchingFilters: ', die allen aktiven Filtern entsprechen',
filtersAffectStats:
'Filter im linken Bereich werden hier angewendet: Werte, Diagramme und Immobilienzahlen nutzen die {{count}} aktiven Filter.',
'Filter werden hier angewendet: Werte, Diagramme und Immobilienzahlen nutzen die {{count}} aktiven Filter.',
noFiltersAffectStats:
'Filter im linken Bereich aktualisieren diesen Bereich: Fügen Sie Filter hinzu, um diese Werte für passende Immobilien neu zu berechnen.',
'Fügen Sie Filter hinzu, um diese Werte für passende Immobilien neu zu berechnen.',
noFilteredMatches: 'Keine Immobilien in diesem Gebiet entsprechen Ihren Filtern.',
unfilteredAreaCount:
'{{count}} Immobilien gibt es hier vor den Filtern; der Ort ist gültig, wird aber herausgefiltert.',
@ -274,6 +275,7 @@ const de: Translations = {
'In diesem ausgewählten Gebiet wurden auch vor den Filtern keine Immobilien gefunden.',
relaxFiltersHint: 'Lockern oder löschen Sie Filter, um Immobilien in diesem Gebiet zu sehen.',
viewProperties: '{{count}} Immobilien ansehen',
viewPropertiesShort: 'Immobilien ansehen',
priceHistory: 'Preisentwicklung',
journeysFrom: 'Verbindungen ab {{label}}',
to: 'Nach {{destination}}',
@ -497,7 +499,7 @@ const de: Translations = {
dsIodName: 'English Indices of Deprivation 2025',
dsIodOrigin: 'Ministry of Housing, Communities & Local Government',
dsIodUse:
'Relative Benachteiligungswerte für Einkommen, Beschäftigung, Bildung, Gesundheit, Kriminalität und Wohnumfeld für jedes Viertel in England.',
'Nationale Benachteiligungsperzentile für Einkommen, Beschäftigung, Bildung, Gesundheit, Kriminalität und Wohnumfeld für jedes Viertel in England.',
dsEthnicityName: 'Bevölkerung nach Ethnie (Zensus 2021)',
dsEthnicityOrigin: 'ONS',
dsEthnicityUse:
@ -914,12 +916,15 @@ const de: Translations = {
// ─ POI group names ─
'Public Transport': 'Öffentlicher Nahverkehr',
Leisure: 'Freizeit',
'Food & Drink': 'Essen & Trinken',
'Green Space': 'Grünflächen',
Health: 'Gesundheit',
'Emergency Services': 'Rettungsdienste',
Groceries: 'Lebensmittel',
'Local Businesses': 'Lokale Geschäfte',
Culture: 'Kultur',
Services: 'Dienstleistungen',
Practical: 'Praktisches',
Shops: 'Geschäfte',
// ─ POI categories ─

View file

@ -218,7 +218,7 @@ const en = {
generatingFilters: 'Generating filters...',
refiningResults: 'Refining results...',
weeklyLimitReached:
'Youve reached the weekly AI usage limit. It will reset automatically next week.',
'Youve reached the weekly AI usage limit. Itll reset automatically next week.',
},
// ── Map Legend ─────────────────────────────────────
@ -257,18 +257,19 @@ const en = {
// ── Area Pane ──────────────────────────────────────
areaPane: {
areaStatistics: 'Area Statistics',
areaOverview: 'Overview',
statsFor: 'Stats for all properties in this {{type}}',
matchingFilters: ' matching all active filters',
filtersAffectStats:
'Left-pane filters are applied here: values, charts, and property counts use the {{count}} active filters.',
noFiltersAffectStats:
'Left-pane filters update this pane: add filters to recalculate these values for matching properties.',
'Filters are applied here: values, charts, and property counts use the {{count}} active filters.',
noFiltersAffectStats: 'Add filters to recalculate these values for matching properties.',
noFilteredMatches: 'No properties match your filters in this area.',
unfilteredAreaCount:
'{{count}} properties exist here before filters, so the location is valid but filtered out.',
noUnfilteredAreaProperties: 'No properties were found in this selected area before filters.',
relaxFiltersHint: 'Relax or clear filters to see properties in this area.',
viewProperties: 'View {{count}} Properties',
viewPropertiesShort: 'View properties',
priceHistory: 'Price History',
journeysFrom: 'Journeys from {{label}}',
to: 'To {{destination}}',
@ -321,7 +322,7 @@ const en = {
searchLabel: 'Search places or postcodes',
locateMe: 'Go to my location',
geolocationUnsupported: 'Geolocation not supported by your browser',
geolocationFailed: 'Could not determine your location',
geolocationFailed: 'Couldnt determine your location',
},
// ── Mobile Drawer ──────────────────────────────────
@ -338,7 +339,7 @@ const en = {
heroSubtitle:
'From London boroughs to commuter towns and regional cities, England has too many places to research one by one.',
heroDescription:
'Set your budget, commute, schools, safety, noise, broadband, and lifestyle needs. Perfect Postcode scans Englands postcodes and reveals the places that actually fit, including areas you would never have typed into a listing portal.',
'Set your budget, commute, schools, safety, noise, broadband, and lifestyle needs. Perfect Postcode scans Englands postcodes and reveals the places that actually fit, including areas youd never have typed into a listing portal.',
exploreTheMap: 'Find my matching postcodes',
seeTheDifference: 'See how it works',
showcaseHeader: 'How it works',
@ -356,7 +357,7 @@ const en = {
showcaseStep1Chip3: 'Under £500k',
showcaseStep1VennCenter: 'Postcodes that meet all three',
showcaseStep2Tab: 'Match',
showcaseStep2Title: 'Let the map surface places you would not have typed',
showcaseStep2Title: 'Let the map surface places you wouldnt have typed',
showcaseStep2Body:
'Scan England by fit instead of starting from familiar area names. Hidden pockets become visible before listing portals narrow your imagination.',
showcaseStep2Region: 'Greater London',
@ -393,9 +394,9 @@ const en = {
statPostcodeInEngland: 'postcode in England',
ourPhilosophy: 'Start with your life, not a postcode',
philosophyP1:
'Most property sites ask where you want to live. In London that is painfully hard, but the same problem shows up across England: buyers choose from the few places they know, then cross-check commute tools, Ofsted, police data, Street View, broadband checkers, and sold prices in separate tabs.',
'Most property sites ask where you want to live. In London thats painfully hard, but the same problem shows up across England: buyers choose from the few places they know, then cross-check commute tools, Ofsted, police data, Street View, broadband checkers, and sold prices in separate tabs.',
philosophyP2:
'Perfect Postcode flips the search. Tell the map what matters and it shows the postcodes that qualify, with evidence for why they are worth inspecting. Data first, then go test the vibe.',
'Perfect Postcode flips the search. Tell the map what matters and it shows the postcodes that qualify, with evidence for why theyre worth inspecting. Data first, then go test the vibe.',
streetTitle: 'Places change street by street',
streetIntro:
'Broad area names hide the details that matter: the station side, the road noise, the school mix, the exact commute, and what similar homes actually sold for.',
@ -490,7 +491,7 @@ const en = {
dsIodName: 'English Indices of Deprivation 2025',
dsIodOrigin: 'Ministry of Housing, Communities & Local Government',
dsIodUse:
'Relative deprivation scores across income, employment, education, health, crime, and living environment for every neighbourhood in England.',
'National deprivation percentiles across income, employment, education, health, crime, and living environment for every neighbourhood in England.',
dsEthnicityName: 'Population by Ethnicity (2021 Census)',
dsEthnicityOrigin: 'ONS',
dsEthnicityUse:
@ -554,8 +555,8 @@ const en = {
// FAQ items — Finding Your Area
faqFinding1Q: 'Where should I look once the obvious areas are too expensive?',
faqFinding1A:
'Start with the things you cannot compromise on: budget, home type, space, commute, schools, safety, noise, broadband, parks, and anything else that matters. The map hides places that do not fit, so less obvious areas can surface before you start scrolling listings.',
faqFinding2Q: 'How do I find good postcodes in places I do not know well?',
'Start with the things you cant compromise on: budget, home type, space, commute, schools, safety, noise, broadband, parks, and anything else that matters. The map hides places that dont fit, so less obvious areas can surface before you start scrolling listings.',
faqFinding2Q: 'How do I find good postcodes in places I dont know well?',
faqFinding2A:
'Set your must-haves across the whole map, then look closely at the groups of places that remain. You can compare unfamiliar postcodes by commute, sold prices, schools, crime, broadband, noise, and shops or parks nearby instead of relying on reputation.',
faqFinding3Q: 'What should I do when my search returns too many or too few areas?',
@ -567,14 +568,14 @@ const en = {
'Travel times are calculated in advance for each saved destination. We work out which postcodes can reach that destination by car, bike, walking, or public transport, then save those results so the map can respond quickly while you filter.',
faqCommute2Q: 'What should I know about the travel-time numbers?',
faqCommute2A:
'Public transport times are based on a weekday morning commute, using departures between 07:30 and 08:30. The normal setting shows a typical journey in that window. These are planning estimates, so they do not include live delays, traffic, or last-minute platform changes.',
'Public transport times are based on a weekday morning commute, using departures between 07:30 and 08:30. The normal setting shows a typical journey in that window. These are planning estimates, so they dont include live delays, traffic, or last-minute platform changes.',
faqCommute3Q: 'When should I use the Best case button?',
faqCommute3A:
'Use the Best case button on public-transport searches when you want to see the journey with a well-timed departure and good connections. Leave it off for the everyday comparison, because the normal setting is closer to what you should expect most days.',
// FAQ items — Budget and Value
faqBudget1Q: 'How do you estimate current property prices?',
faqBudget1A:
'The estimate starts with the homes last recorded sale price from HM Land Registry. We bring that sale up to todays market by looking at how similar homes have changed in value over time, especially homes of the same type nearby. Where there are fewer local sales, the estimate leans more on wider area trends. It is then checked against nearby recent sales and floor area so the result is useful for comparison.',
'The estimate starts with the homes last recorded sale price from HM Land Registry. We bring that sale up to todays market by looking at how similar homes have changed in value over time, especially homes of the same type nearby. Where there are fewer local sales, the estimate leans more on wider area trends. Its then checked against nearby recent sales and floor area so the result is useful for comparison.',
faqBudget2Q: 'Why use estimated current price instead of last sold price?',
faqBudget2A:
'Last sold price can be years or decades old, while asking prices only cover homes listed today. Estimated current price puts older sales into todays market, so you can compare more homes and spot areas that may offer better value before listings appear. Treat it as a guide for shortlisting, not a bank valuation.',
@ -584,12 +585,12 @@ const en = {
'Crime is broken down by type, including violence, burglary, robbery, vehicle crime, antisocial behaviour, shoplifting, drugs, and public order. You can focus on the risks that matter to you instead of relying on one vague safety score.',
faqSafety2Q: 'What should I check before viewing an unfamiliar street?',
faqSafety2A:
'Check crime, road noise, broadband, parks, food shops, schools, and commute before you book. Listing photos are useful, but they should not be the first time you learn what the street is like.',
'Check crime, road noise, broadband, parks, food shops, schools, and commute before you book. Listing photos are useful, but they shouldnt be the first time you learn what the street is like.',
// FAQ items — Families and Schools
faqFamilies1Q: 'Which areas have the right mix of schools, space, safety, and commute?',
faqFamilies1A:
'Put school ratings, crime, parks, commute, space, home type, and budget on one map. The result is a practical family shortlist instead of a pile of separate school, crime, listing, and transport searches.',
faqFamilies2Q: 'Does this prove I am inside a school catchment?',
faqFamilies2Q: 'Does this prove Im inside a school catchment?',
faqFamilies2A:
'No. We show nearby school quality and local education data, but admissions areas and priority rules can change. Use Perfect Postcode to shortlist places, then check catchments and admissions with the school or local council.',
// FAQ items — Environment and Quality of Life
@ -601,14 +602,14 @@ const en = {
'Not today. We show things such as road noise, energy rating, building age, and the local environment around the postcode. Flood risk, legal issues, structural issues, mortgage concerns, and survey findings still need to be checked separately before you buy.',
faqEnv3Q: 'What running-cost checks can I do before viewing?',
faqEnv3A:
'You can check energy rating, floor area, building age, council tax area, broadband, and noise before viewing. It will not predict your exact bills, but it helps you avoid obvious mismatches early.',
'You can check energy rating, floor area, building age, council tax area, broadband, and noise before viewing. It wont predict your exact bills, but it helps you avoid obvious mismatches early.',
// FAQ items — Listing Portals and Due Diligence
faqDueDiligence1Q: 'Should I use this before or after checking Rightmove?',
faqDueDiligence1A:
'Use Perfect Postcode before and alongside listing sites. Rightmove, Zoopla, and OnTheMarket are still where you check what is for sale now, photos, agents, viewings, and alerts. Perfect Postcode helps you decide which postcodes are worth searching in the first place.',
'Use Perfect Postcode before and alongside listing sites. Rightmove, Zoopla, and OnTheMarket are still where you check whats for sale now, photos, agents, viewings, and alerts. Perfect Postcode helps you decide which postcodes are worth searching in the first place.',
faqDueDiligence2Q: 'Why cant I filter by garden, garage, or layout?',
faqDueDiligence2A:
'Those details are not available consistently for every home. Perfect Postcode can filter by things such as floor area, home type, ownership type, energy rating, sold prices, and local area data. Gardens, garages, aspect, room layout, and estate-agent wording still need to be checked in the listing and at the viewing.',
'Those details arent available consistently for every home. Perfect Postcode can filter by things such as floor area, home type, ownership type, energy rating, sold prices, and local area data. Gardens, garages, aspect, room layout, and estate-agent wording still need to be checked in the listing and at the viewing.',
faqDueDiligence3Q: 'Do you track listing price cuts and time on market?',
faqDueDiligence3A:
'Not currently. Perfect Postcode is built around sold prices, energy ratings, postcode data, travel times, and neighbourhood data rather than live listing changes. You can still use sale history, estimated current value, and price per square metre to judge whether an asking price looks stretched.',
@ -618,9 +619,9 @@ const en = {
// FAQ items — Privacy and Data Protection
faqPrivacy1Q: 'Do you store personal data about me?',
faqPrivacy1A:
'The property and neighbourhood data does not contain your personal details. If you create an account, we store only what is needed to run the service, such as your email address, access status, newsletter choice, saved searches, saved properties, and payment records handled by Stripe. We handle account data under UK privacy law.',
'The property and neighbourhood data doesnt contain your personal details. If you create an account, we store only whats needed to run the service, such as your email address, access status, newsletter choice, saved searches, saved properties, and payment records handled by Stripe. We handle account data under UK privacy law.',
// FAQ items — Why Perfect Postcode
faqWhy1Q: 'What does this show that listing portals usually do not?',
faqWhy1Q: 'What does this show that listing portals usually dont?',
faqWhy1A:
'Listing sites start from homes that are for sale right now. Perfect Postcode starts from the places that fit your life and budget, using sold prices, space, commute, schools, crime, noise, broadband, energy rating, ownership type, and local amenities before you open the listings.',
faqWhy2Q: 'How much manual research does this save?',
@ -628,14 +629,14 @@ const en = {
'You could do the research yourself, but it means checking sold prices, energy ratings, crime, schools, broadband, local facts, environment details, travel times, and maps one postcode at a time. Perfect Postcode puts those sources in one searchable map for England.',
faqWhy3Q: 'How reliable is the data?',
faqWhy3A:
'The main sources are official or widely used public data, including sold prices, energy ratings, local facts, school ratings, broadband, crime, environment, map, and street data. They are useful for shortlisting and comparison, but any purchase decision still needs current checks and expert advice where needed.',
'The main sources are official or widely used public data, including sold prices, energy ratings, local facts, school ratings, broadband, crime, environment, map, and street data. Theyre useful for shortlisting and comparison, but any purchase decision still needs current checks and expert advice where needed.',
// FAQ items — Pricing and Access
faqPricing1Q: 'Why pay when postcode reports are free?',
faqPricing1A:
'Free postcode tools are useful once you already know what to check. Perfect Postcode is for scanning every postcode in England against your needs, combining filters, comparing options, saving searches, and exporting a shortlist before you spend weekends on viewings.',
faqPricing2Q: 'What does lifetime access mean?',
faqPricing2A:
'Lifetime access means one payment gives your account ongoing access to the paid Perfect Postcode map for as long as the service runs. It is not a monthly or annual subscription, and normal data updates are included. You can use it during this search, come back later, and still have access if you move again.',
'Lifetime access means one payment gives your account ongoing access to the paid Perfect Postcode map for as long as the service runs. It isnt a monthly or annual subscription, and normal data updates are included. You can use it during this search, come back later, and still have access if you move again.',
faqPricing3Q: 'What can I access on the free tier?',
faqPricing3A:
'Free users can explore all features within the demo area: inner London, roughly zones 1 to 2. To access data for the rest of England, you need lifetime access.',
@ -649,7 +650,7 @@ const en = {
'Click the i info button next to a filter or feature to open a short explanation of what it means and how to read it. Some areas of the map, such as travel-time cards, also have their own info button.',
faqTips3Q: 'How do I refresh the map colours?',
faqTips3A:
'When an eye preview is colouring the map, use Reset colour scale in the map legend to refresh the colours for the results you are looking at now. This is useful after moving the map, zooming, or changing filters.',
'When an eye preview is colouring the map, use Reset colour scale in the map legend to refresh the colours for the results youre looking at now. This is useful after moving the map, zooming, or changing filters.',
},
// ── Account Page ───────────────────────────────────
@ -676,11 +677,10 @@ const en = {
clickToRename: 'Click to rename',
notesPlaceholder: 'Jot down your thoughts...',
deleteSearch: 'Delete search',
deleteSearchConfirm:
'Are you sure you want to delete this saved search? This cannot be undone.',
deleteSearchConfirm: 'Are you sure you want to delete this saved search? This cant be undone.',
deleteProperty: 'Delete property',
deletePropertyConfirm:
'Are you sure you want to delete this saved property? This cannot be undone.',
'Are you sure you want to delete this saved property? This cant be undone.',
bed: 'bed',
epc: 'EPC',
},
@ -710,7 +710,7 @@ const en = {
specialOffer: 'Special offer!',
invitedByFree: '{{name}} has invited you to get free lifetime access.',
invitedByDiscount: '{{name}} has shared a 30% discount on lifetime access.',
genericFreeInvite: 'You have been invited to get free lifetime access.',
genericFreeInvite: 'Youve been invited to get free lifetime access.',
genericDiscount: 'A friend has shared a 30% discount on lifetime access.',
exploreEvery: 'Find postcodes that fit your life',
propertyInfo: 'Prices, commute, schools, crime, noise, broadband, EPC and more',
@ -900,12 +900,15 @@ const en = {
// ─ POI group names ─
'Public Transport': 'Public Transport',
Leisure: 'Leisure',
'Food & Drink': 'Food & Drink',
'Green Space': 'Green Space',
Health: 'Health',
'Emergency Services': 'Emergency Services',
Groceries: 'Groceries',
'Local Businesses': 'Local Businesses',
Culture: 'Culture',
Services: 'Services',
Practical: 'Practical',
Shops: 'Shops',
// ─ POI categories ─

View file

@ -264,12 +264,13 @@ const fr: Translations = {
// ── Area Pane ──────────────────────────────────────
areaPane: {
areaStatistics: 'Statistiques de la zone',
areaOverview: 'Vue densemble',
statsFor: 'Statistiques pour toutes les propriétés de ce/cette {{type}}',
matchingFilters: ' correspondant à tous les filtres actifs',
filtersAffectStats:
'Les filtres du panneau de gauche sont appliqués ici : valeurs, graphiques et nombres de propriétés utilisent les {{count}} filtres actifs.',
'Les filtres sont appliqués ici : valeurs, graphiques et nombres de propriétés utilisent les {{count}} filtres actifs.',
noFiltersAffectStats:
'Les filtres du panneau de gauche mettent ce panneau à jour : ajoutez des filtres pour recalculer ces valeurs pour les propriétés correspondantes.',
'Ajoutez des filtres pour recalculer ces valeurs pour les propriétés correspondantes.',
noFilteredMatches: 'Aucune propriété de cette zone ne correspond à vos filtres.',
unfilteredAreaCount:
'{{count}} propriétés existent ici avant les filtres ; le lieu est valide, mais filtré.',
@ -277,6 +278,7 @@ const fr: Translations = {
'Aucune propriété na été trouvée dans cette zone sélectionnée avant les filtres.',
relaxFiltersHint: 'Assouplissez ou effacez les filtres pour voir les propriétés de cette zone.',
viewProperties: 'Voir {{count}} propriétés',
viewPropertiesShort: 'Voir les propriétés',
priceHistory: 'Historique des prix',
journeysFrom: 'Trajets depuis {{label}}',
to: 'Vers {{destination}}',
@ -499,7 +501,7 @@ const fr: Translations = {
dsIodName: 'English Indices of Deprivation 2025',
dsIodOrigin: 'Ministry of Housing, Communities & Local Government',
dsIodUse:
'Scores de défaveur relative couvrant le revenu, lemploi, léducation, la santé, la criminalité et le cadre de vie pour chaque quartier dAngleterre.',
'Percentiles nationaux de défaveur couvrant le revenu, lemploi, léducation, la santé, la criminalité et le cadre de vie pour chaque quartier dAngleterre.',
dsEthnicityName: 'Population par ethnie (recensement 2021)',
dsEthnicityOrigin: 'ONS',
dsEthnicityUse:
@ -913,12 +915,15 @@ const fr: Translations = {
// ─ POI group names ─
'Public Transport': 'Transports en commun',
Leisure: 'Loisirs',
'Food & Drink': 'Restauration',
'Green Space': 'Espaces verts',
Health: 'Santé',
'Emergency Services': 'Services durgence',
Groceries: 'Alimentation',
'Local Businesses': 'Commerces de proximité',
Culture: 'Culture',
Services: 'Services',
Practical: 'Services pratiques',
Shops: 'Boutiques',
// ─ POI categories ─

View file

@ -244,18 +244,20 @@ const hi: Translations = {
areaPane: {
areaStatistics: 'क्षेत्र आंकड़े',
areaOverview: 'अवलोकन',
statsFor: 'इस {{type}} की सभी संपत्तियों के आंकड़े',
matchingFilters: ' सभी सक्रिय फिल्टर से मेल खाते हुए',
filtersAffectStats:
'बाएं पैनल के फिल्टर यहां लागू होते हैं: मान, चार्ट और संपत्ति संख्या {{count}} सक्रिय फिल्टर का उपयोग करते हैं.',
'फिल्टर यहां लागू होते हैं: मान, चार्ट और संपत्ति संख्या {{count}} सक्रिय फिल्टर का उपयोग करते हैं.',
noFiltersAffectStats:
'बाएं पैनल के फिल्टर इस पैनल को अपडेट करते हैं: मेल खाने वाली संपत्तियों के लिए ये मान फिर गणना करने हेतु फिल्टर जोड़ें.',
'मेल खाने वाली संपत्तियों के लिए ये मान फिर गणना करने हेतु फिल्टर जोड़ें.',
noFilteredMatches: 'इस क्षेत्र में कोई संपत्ति आपके फिल्टर से मेल नहीं खाती.',
unfilteredAreaCount:
'फिल्टर से पहले यहां {{count}} संपत्तियां हैं, इसलिए स्थान वैध है लेकिन फिल्टर से बाहर हो गया है.',
noUnfilteredAreaProperties: 'फिल्टर से पहले इस चुने हुए क्षेत्र में कोई संपत्ति नहीं मिली.',
relaxFiltersHint: 'इस क्षेत्र की संपत्तियां देखने के लिए फिल्टर ढीले करें या साफ करें.',
viewProperties: '{{count}} संपत्तियां देखें',
viewPropertiesShort: 'संपत्तियां देखें',
priceHistory: 'कीमत इतिहास',
journeysFrom: '{{label}} से यात्राएं',
to: '{{destination}} तक',
@ -466,7 +468,7 @@ const hi: Translations = {
dsIodName: 'English Indices of Deprivation 2025',
dsIodOrigin: 'Ministry of Housing, Communities & Local Government',
dsIodUse:
'इंग्लैंड के हर neighbourhood के लिए income, employment, education, health, crime और living environment में relative deprivation scores.',
'इंग्लैंड के हर neighbourhood के लिए income, employment, education, health, crime और living environment में national deprivation percentiles.',
dsEthnicityName: 'Population by Ethnicity (2021 Census)',
dsEthnicityOrigin: 'ONS',
dsEthnicityUse:
@ -829,12 +831,15 @@ const hi: Translations = {
'Political vote share': 'राजनीतिक वोट शेयर',
'Public Transport': 'सार्वजनिक परिवहन',
Leisure: 'अवकाश',
'Food & Drink': 'खाना-पीना',
'Green Space': 'हरित क्षेत्र',
Health: 'स्वास्थ्य',
'Emergency Services': 'आपातकालीन सेवाएं',
Groceries: 'किराना',
'Local Businesses': 'स्थानीय व्यवसाय',
Culture: 'संस्कृति',
Services: 'सेवाएं',
Practical: 'व्यावहारिक',
Shops: 'दुकानें',
Airport: 'हवाई अड्डा',
Ferry: 'फेरी',

View file

@ -259,18 +259,20 @@ const hu: Translations = {
// ── Area Pane ──────────────────────────────────────
areaPane: {
areaStatistics: 'Területi statisztikák',
areaOverview: 'Áttekintés',
statsFor: 'Statisztikák a(z) {{type}} összes ingatlanáról',
matchingFilters: ' az összes aktív szűrőnek megfelelően',
filtersAffectStats:
'A bal oldali panel szűrői itt is érvényesek: az értékek, diagramok és ingatlanszámok a(z) {{count}} aktív szűrőt használják.',
'A szűrők itt is érvényesek: az értékek, diagramok és ingatlanszámok a(z) {{count}} aktív szűrőt használják.',
noFiltersAffectStats:
'A bal oldali panel szűrői frissítik ezt a panelt: adjon hozzá szűrőket, hogy ezek az értékek az illeszkedő ingatlanokra számolódjanak újra.',
'Adjon hozzá szűrőket, hogy ezek az értékek az illeszkedő ingatlanokra számolódjanak újra.',
noFilteredMatches: 'Ezen a területen egyetlen ingatlan sem felel meg a szűrőknek.',
unfilteredAreaCount:
'{{count}} ingatlan található itt szűrők nélkül, tehát a hely érvényes, csak a szűrők kizárják.',
noUnfilteredAreaProperties: 'A kiválasztott területen szűrők nélkül sem található ingatlan.',
relaxFiltersHint: 'Lazítson vagy törölje a szűrőket, hogy lássa a terület ingatlanjait.',
viewProperties: '{{count}} ingatlan megtekintése',
viewPropertiesShort: 'Ingatlanok megtekintése',
priceHistory: 'Ártörténet',
journeysFrom: 'Utazások innen: {{label}}',
to: 'Ide: {{destination}}',
@ -493,7 +495,7 @@ const hu: Translations = {
dsIodName: 'Angol Deprivációs Mutatók 2025',
dsIodOrigin: 'Ministry of Housing, Communities & Local Government',
dsIodUse:
'Relatív deprivációs pontok jövedelem, foglalkoztatottság, oktatás, egészség, bűnözés és lakókörnyezet területén Anglia minden szomszédságára.',
'Országos deprivációs percentilisek jövedelem, foglalkoztatottság, oktatás, egészség, bűnözés és lakókörnyezet területén Anglia minden szomszédságára.',
dsEthnicityName: 'Népesség etnikai megoszlás szerint (2021-es népszámlálás)',
dsEthnicityOrigin: 'ONS',
dsEthnicityUse:
@ -907,12 +909,15 @@ const hu: Translations = {
// ─ POI group names ─
'Public Transport': 'Tömegközlekedés',
Leisure: 'Szabadidő',
'Food & Drink': 'Étel és ital',
'Green Space': 'Zöldterületek',
Health: 'Egészségügy',
'Emergency Services': 'Sürgősségi szolgálatok',
Groceries: 'Élelmiszer',
'Local Businesses': 'Helyi vállalkozások',
Culture: 'Kultúra',
Services: 'Szolgáltatások',
Practical: 'Praktikus',
Shops: 'Üzletek',
// ─ POI categories ─

View file

@ -256,17 +256,18 @@ const zh: Translations = {
// ── Area Pane ──────────────────────────────────────
areaPane: {
areaStatistics: '区域统计',
areaOverview: '概览',
statsFor: '该{{type}}内所有房产的统计数据',
matchingFilters: ',满足所有当前筛选条件',
filtersAffectStats:
'左侧面板的筛选条件会应用到这里:数值、图表和房产数量都会使用 {{count}} 个当前筛选条件。',
noFiltersAffectStats:
'左侧面板的筛选条件会更新此面板:添加筛选条件后,这些值会按匹配的房产重新计算。',
'筛选条件会应用到这里:数值、图表和房产数量都会使用 {{count}} 个当前筛选条件。',
noFiltersAffectStats: '添加筛选条件后,这些值会按匹配的房产重新计算。',
noFilteredMatches: '该区域没有房产符合当前筛选条件。',
unfilteredAreaCount: '筛选前这里有 {{count}} 处房产;位置有效,但被筛选条件排除了。',
noUnfilteredAreaProperties: '筛选前该选定区域内也没有找到房产。',
relaxFiltersHint: '放宽或清除筛选条件即可查看该区域的房产。',
viewProperties: '查看 {{count}} 处房产',
viewPropertiesShort: '查看房产',
priceHistory: '价格历史',
journeysFrom: '从 {{label}} 出发的路线',
to: '到 {{destination}}',
@ -482,7 +483,7 @@ const zh: Translations = {
dsNsplUse: '将邮编映射到坐标和统计区域代码,用于将所有区域级数据集关联到各个房产。',
dsIodName: 'English Indices of Deprivation 2025',
dsIodOrigin: 'Ministry of Housing, Communities & Local Government',
dsIodUse: '英格兰每个社区在收入、就业、教育、健康、犯罪和居住环境方面的相对贫困指数。',
dsIodUse: '英格兰每个社区在收入、就业、教育、健康、犯罪和居住环境方面的全国贫困百分位。',
dsEthnicityName: '按族裔划分的人口2021 年人口普查)',
dsEthnicityOrigin: 'ONS',
dsEthnicityUse:
@ -880,12 +881,15 @@ const zh: Translations = {
// ─ POI group names ─
'Public Transport': '公共交通',
Leisure: '休闲',
'Food & Drink': '餐饮',
'Green Space': '绿地',
Health: '健康',
'Emergency Services': '紧急服务',
Groceries: '食品杂货',
'Local Businesses': '本地商业',
Culture: '文化',
Services: '服务',
Practical: '实用服务',
Shops: '商店',
// ─ POI categories ─

View file

@ -25,6 +25,7 @@ dependencies = [
"rasterio>=1.5.0",
"pyproj>=3.7.2",
"pyshp>=2.3.0",
"pillow>=12.0.0",
"folium>=0.20.0",
"httpx",
"polars",
@ -41,8 +42,8 @@ dev = [
]
[tool.deptry]
# analyses/ and scripts/ use transitive deps
exclude = ["\\.venv", "analyses", "scripts"]
# analyses/ and scripts/ use transitive deps; video/tts has its own UV project.
exclude = ["\\.venv", "analyses", "scripts", "video/tts"]
[tool.deptry.per_rule_ignores]
# pyarrow/fastexcel: runtime backends for polars parquet/Excel I/O

2
uv.lock generated
View file

@ -1376,6 +1376,7 @@ dependencies = [
{ name = "numpy", marker = "python_full_version < '3.14' and sys_platform == 'linux'" },
{ name = "osmium", marker = "python_full_version < '3.14' and sys_platform == 'linux'" },
{ name = "pandas", marker = "python_full_version < '3.14' and sys_platform == 'linux'" },
{ name = "pillow", marker = "python_full_version < '3.14' and sys_platform == 'linux'" },
{ name = "plotly", marker = "python_full_version < '3.14' and sys_platform == 'linux'" },
{ name = "polars", marker = "python_full_version < '3.14' and sys_platform == 'linux'" },
{ name = "pyarrow", marker = "python_full_version < '3.14' and sys_platform == 'linux'" },
@ -1407,6 +1408,7 @@ requires-dist = [
{ name = "numpy", specifier = ">=1.26.0" },
{ name = "osmium", specifier = ">=4.0.0" },
{ name = "pandas", specifier = ">=2.0.0" },
{ name = "pillow", specifier = ">=12.0.0" },
{ name = "plotly", specifier = ">=6.5.2" },
{ name = "polars" },
{ name = "polars", specifier = ">=1.37.1" },