diff --git a/.gitignore b/.gitignore
index d84dc57..b415284 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,5 +5,7 @@
**/dist
server-rs/target
.task
-frontend/public/assets
+frontend/public/assets/*
+!frontend/public/assets/poi-icons/
+!frontend/public/assets/poi-icons/**
server-rs/logs
diff --git a/frontend/public/assets/poi-icons/asda/asda_express_24px.svg b/frontend/public/assets/poi-icons/asda/asda_express_24px.svg
new file mode 100644
index 0000000..60ed62c
--- /dev/null
+++ b/frontend/public/assets/poi-icons/asda/asda_express_24px.svg
@@ -0,0 +1,64 @@
+
+
diff --git a/frontend/public/assets/poi-icons/asda/asda_green_basket_24px.svg b/frontend/public/assets/poi-icons/asda/asda_green_basket_24px.svg
new file mode 100644
index 0000000..8dd2806
--- /dev/null
+++ b/frontend/public/assets/poi-icons/asda/asda_green_basket_24px.svg
@@ -0,0 +1,63 @@
+
+
diff --git a/frontend/public/assets/poi-icons/asda/asda_green_trolley_24px.svg b/frontend/public/assets/poi-icons/asda/asda_green_trolley_24px.svg
new file mode 100644
index 0000000..cddc2b6
--- /dev/null
+++ b/frontend/public/assets/poi-icons/asda/asda_green_trolley_24px.svg
@@ -0,0 +1,130 @@
+
+
diff --git a/frontend/public/assets/poi-icons/asda/asda_living_24px.svg b/frontend/public/assets/poi-icons/asda/asda_living_24px.svg
new file mode 100644
index 0000000..e545024
--- /dev/null
+++ b/frontend/public/assets/poi-icons/asda/asda_living_24px.svg
@@ -0,0 +1,56 @@
+
+
diff --git a/frontend/public/assets/poi-icons/asda/asda_pfs_24px.svg b/frontend/public/assets/poi-icons/asda/asda_pfs_24px.svg
new file mode 100644
index 0000000..8648809
--- /dev/null
+++ b/frontend/public/assets/poi-icons/asda/asda_pfs_24px.svg
@@ -0,0 +1,66 @@
+
+
diff --git a/frontend/public/assets/poi-icons/asda/asda_primary.svg b/frontend/public/assets/poi-icons/asda/asda_primary.svg
new file mode 100644
index 0000000..5619073
--- /dev/null
+++ b/frontend/public/assets/poi-icons/asda/asda_primary.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/asda/asda_superstore_green_trolley_24px.svg b/frontend/public/assets/poi-icons/asda/asda_superstore_green_trolley_24px.svg
new file mode 100644
index 0000000..d0e5f44
--- /dev/null
+++ b/frontend/public/assets/poi-icons/asda/asda_superstore_green_trolley_24px.svg
@@ -0,0 +1,297 @@
+
+
diff --git a/frontend/public/assets/poi-icons/brands/aldi_24px.svg b/frontend/public/assets/poi-icons/brands/aldi_24px.svg
new file mode 100644
index 0000000..d3d253c
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/aldi_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/amazon_fresh_alt_24px.svg b/frontend/public/assets/poi-icons/brands/amazon_fresh_alt_24px.svg
new file mode 100644
index 0000000..a27368f
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/amazon_fresh_alt_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/booths_24px.svg b/frontend/public/assets/poi-icons/brands/booths_24px.svg
new file mode 100644
index 0000000..b49b922
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/booths_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/budgens_24px.svg b/frontend/public/assets/poi-icons/brands/budgens_24px.svg
new file mode 100644
index 0000000..e54b050
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/budgens_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/centra_24px.svg b/frontend/public/assets/poi-icons/brands/centra_24px.svg
new file mode 100644
index 0000000..7fe39a7
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/centra_24px.svg
@@ -0,0 +1,93 @@
+
+
+
+
diff --git a/frontend/public/assets/poi-icons/brands/cook.svg b/frontend/public/assets/poi-icons/brands/cook.svg
new file mode 100644
index 0000000..c9742ff
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/cook.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/coop_24px.svg b/frontend/public/assets/poi-icons/brands/coop_24px.svg
new file mode 100644
index 0000000..abded3b
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/coop_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/costco_24px.svg b/frontend/public/assets/poi-icons/brands/costco_24px.svg
new file mode 100644
index 0000000..1bae663
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/costco_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/dunnes_stores_24px.svg b/frontend/public/assets/poi-icons/brands/dunnes_stores_24px.svg
new file mode 100644
index 0000000..83edd9d
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/dunnes_stores_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/farmfoods_updated_24px.svg b/frontend/public/assets/poi-icons/brands/farmfoods_updated_24px.svg
new file mode 100644
index 0000000..b881b3e
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/farmfoods_updated_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/heron_24px.svg b/frontend/public/assets/poi-icons/brands/heron_24px.svg
new file mode 100644
index 0000000..5278db6
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/heron_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/iceland_24px.svg b/frontend/public/assets/poi-icons/brands/iceland_24px.svg
new file mode 100644
index 0000000..e7f87e0
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/iceland_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/iceland_food_warehouse_24px.svg b/frontend/public/assets/poi-icons/brands/iceland_food_warehouse_24px.svg
new file mode 100644
index 0000000..adeee4d
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/iceland_food_warehouse_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/lidl_24px.svg b/frontend/public/assets/poi-icons/brands/lidl_24px.svg
new file mode 100644
index 0000000..d14d7fa
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/lidl_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/little_waitrose_24px.svg b/frontend/public/assets/poi-icons/brands/little_waitrose_24px.svg
new file mode 100644
index 0000000..6a91ca5
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/little_waitrose_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/makro_24px.svg b/frontend/public/assets/poi-icons/brands/makro_24px.svg
new file mode 100644
index 0000000..ff03a03
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/makro_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/mns_24px.svg b/frontend/public/assets/poi-icons/brands/mns_24px.svg
new file mode 100644
index 0000000..d5daa4e
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/mns_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/mns_food_24px.svg b/frontend/public/assets/poi-icons/brands/mns_food_24px.svg
new file mode 100644
index 0000000..77789ad
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/mns_food_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/mns_high_street_24px.svg b/frontend/public/assets/poi-icons/brands/mns_high_street_24px.svg
new file mode 100644
index 0000000..5fbc1e5
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/mns_high_street_24px.svg
@@ -0,0 +1,169 @@
+
+
+
+
diff --git a/frontend/public/assets/poi-icons/brands/mns_hospital_24px.svg b/frontend/public/assets/poi-icons/brands/mns_hospital_24px.svg
new file mode 100644
index 0000000..8f21a16
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/mns_hospital_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/mns_moto_24px.svg b/frontend/public/assets/poi-icons/brands/mns_moto_24px.svg
new file mode 100644
index 0000000..4534574
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/mns_moto_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/mns_outlet_24px.svg b/frontend/public/assets/poi-icons/brands/mns_outlet_24px.svg
new file mode 100644
index 0000000..47becd4
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/mns_outlet_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/morrisons_24px.svg b/frontend/public/assets/poi-icons/brands/morrisons_24px.svg
new file mode 100644
index 0000000..4f8c564
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/morrisons_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/morrisons_daily_24px.svg b/frontend/public/assets/poi-icons/brands/morrisons_daily_24px.svg
new file mode 100644
index 0000000..21b2db1
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/morrisons_daily_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/sainsburys_24px.svg b/frontend/public/assets/poi-icons/brands/sainsburys_24px.svg
new file mode 100644
index 0000000..045ff95
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/sainsburys_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/sainsburys_local_24px.svg b/frontend/public/assets/poi-icons/brands/sainsburys_local_24px.svg
new file mode 100644
index 0000000..2f9b9ca
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/sainsburys_local_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/spar_24px.svg b/frontend/public/assets/poi-icons/brands/spar_24px.svg
new file mode 100644
index 0000000..111b0b6
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/spar_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/tesco_24px.svg b/frontend/public/assets/poi-icons/brands/tesco_24px.svg
new file mode 100644
index 0000000..822eb8a
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/tesco_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/tesco_express_24px.svg b/frontend/public/assets/poi-icons/brands/tesco_express_24px.svg
new file mode 100644
index 0000000..51245ad
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/tesco_express_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/tesco_extra_24px.svg b/frontend/public/assets/poi-icons/brands/tesco_extra_24px.svg
new file mode 100644
index 0000000..94958f4
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/tesco_extra_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/waitrose_24px.svg b/frontend/public/assets/poi-icons/brands/waitrose_24px.svg
new file mode 100644
index 0000000..c742c90
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/waitrose_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/brands/wholefoods_24px.svg b/frontend/public/assets/poi-icons/brands/wholefoods_24px.svg
new file mode 100644
index 0000000..dab7994
--- /dev/null
+++ b/frontend/public/assets/poi-icons/brands/wholefoods_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/logos/planet_organic_24px.svg b/frontend/public/assets/poi-icons/logos/planet_organic_24px.svg
new file mode 100644
index 0000000..dc5cd60
--- /dev/null
+++ b/frontend/public/assets/poi-icons/logos/planet_organic_24px.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/assets/poi-icons/public_transport/london_tube.svg b/frontend/public/assets/poi-icons/public_transport/london_tube.svg
new file mode 100644
index 0000000..09b0483
--- /dev/null
+++ b/frontend/public/assets/poi-icons/public_transport/london_tube.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/components/home/HexCanvas.tsx b/frontend/src/components/home/HexCanvas.tsx
index 16d8ae3..cae40a1 100644
--- a/frontend/src/components/home/HexCanvas.tsx
+++ b/frontend/src/components/home/HexCanvas.tsx
@@ -68,7 +68,9 @@ export default function HexCanvas({
height: (hex.size * 2) / Math.sqrt(3),
opacity: hex.opacity * (isDark ? 0.45 : 0.6),
clipPath: 'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)',
- animation: animated ? `hex-bob ${hex.bobDuration}s ease-in-out infinite` : undefined,
+ animation: animated
+ ? `hex-bob ${hex.bobDuration}s ease-in-out infinite`
+ : undefined,
'--bob': `${hex.bobAmount}px`,
} as React.CSSProperties
}
diff --git a/frontend/src/components/home/HomePage.tsx b/frontend/src/components/home/HomePage.tsx
index 5664963..f19072f 100644
--- a/frontend/src/components/home/HomePage.tsx
+++ b/frontend/src/components/home/HomePage.tsx
@@ -281,8 +281,8 @@ const INSPECT_JOURNEYS: JourneyInstructionPreset[] = [
bestMinutes: 29,
legs: [
{ mode: 'Victoria', from: 'Oxford Circus Underground Station', to: 'Victoria', minutes: 4 },
- { mode: 'District', from: 'Victoria', to: 'Earl\'s Court', minutes: 10 },
- { mode: 'walk', from: 'Earl\'s Court', to: 'SW5 9AA', minutes: 7 },
+ { mode: 'District', from: 'Victoria', to: "Earl's Court", minutes: 10 },
+ { mode: 'walk', from: "Earl's Court", to: 'SW5 9AA', minutes: 7 },
],
},
];
@@ -478,8 +478,7 @@ function FilterPreviewRow({
- +
- {withoutCount.toLocaleString()}
+ +{withoutCount.toLocaleString()}
{' without this filter'}
@@ -946,75 +945,73 @@ function ScoutScreen({ isActive }: { isActive: boolean }) {
-
-
-
-
-
-
- {t('home.showcaseStep4FileName')}
-
-
-
- Top 3
-
-
-
- {[
- t('home.showcaseStep4ColPostcode'),
- t('home.showcaseStep4ColScore'),
- t('home.showcaseStep4ColCommute'),
- t('home.showcaseStep4ColPrice'),
- ].map((heading) => (
-
- {heading}
-
- ))}
-
- {scoutRows.map((row, index) => (
-
-
-
- {index + 1}
-
- {row.postcode}
-
-
-
-
- {row.score}
+
+
+
+
-
-
+
+ {t('home.showcaseStep4FileName')}
+
+ Top 3
+
-
- {row.commute}
+
+ {[
+ t('home.showcaseStep4ColPostcode'),
+ t('home.showcaseStep4ColScore'),
+ t('home.showcaseStep4ColCommute'),
+ t('home.showcaseStep4ColPrice'),
+ ].map((heading) => (
+
+ {heading}
+
+ ))}
-
{row.price}
-
- ))}
+ {scoutRows.map((row, index) => (
+
+
+
+ {index + 1}
+
+ {row.postcode}
+
+
+
+
+ {row.score}
+
+
+
+
+
+
+
+ {row.commute}
+
+
{row.price}
+
+ ))}
@@ -1262,10 +1259,7 @@ export default function HomePage({
if (!scroller) return;
const start = scroller.scrollTop;
const end =
- start +
- target.getBoundingClientRect().top -
- scroller.getBoundingClientRect().top +
- 24;
+ start + target.getBoundingClientRect().top - scroller.getBoundingClientRect().top + 24;
const distance = end - start;
const duration = 1200;
let startTime: number;
@@ -1364,9 +1358,7 @@ export default function HomePage({
{/* Our philosophy */}
-
- {t('home.ourPhilosophy')}
-
+
{t('home.ourPhilosophy')}
{t('home.philosophyP1')}
{highlightBrandText(t('home.philosophyP2'))}
diff --git a/frontend/src/components/map/Map.tsx b/frontend/src/components/map/Map.tsx
index 3fc956e..87964e6 100644
--- a/frontend/src/components/map/Map.tsx
+++ b/frontend/src/components/map/Map.tsx
@@ -84,7 +84,11 @@ interface Dimensions {
height: number;
}
-function resolveInset(pixelValue: number | undefined, ratioValue: number | undefined, size: number) {
+function resolveInset(
+ pixelValue: number | undefined,
+ ratioValue: number | undefined,
+ size: number
+) {
return Math.max(0, (pixelValue ?? 0) + (ratioValue ?? 0) * size);
}
@@ -122,8 +126,7 @@ function getViewportRelativeVisibleAreaCenter(
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const viewportLeft = resolveInset(area.left, area.leftRatio, viewportWidth);
- const viewportRight =
- viewportWidth - resolveInset(area.right, area.rightRatio, viewportWidth);
+ const viewportRight = viewportWidth - resolveInset(area.right, area.rightRatio, viewportWidth);
const viewportTop = resolveInset(area.top, area.topRatio, viewportHeight);
const viewportBottom =
viewportHeight - resolveInset(area.bottom, area.bottomRatio, viewportHeight);
@@ -532,7 +535,12 @@ export default memo(function Map({

{
- mobileDrawerPanelRectRef.current = rect;
- consumePendingCurrentLocationFlyTo(rect);
- }, [consumePendingCurrentLocationFlyTo]);
+ const handleMobileDrawerPanelRectChange = useCallback(
+ (rect: DOMRectReadOnly) => {
+ mobileDrawerPanelRectRef.current = rect;
+ consumePendingCurrentLocationFlyTo(rect);
+ },
+ [consumePendingCurrentLocationFlyTo]
+ );
const handleMobileDrawerClose = useCallback(() => {
pendingCurrentLocationFlyToRef.current = null;
diff --git a/frontend/src/components/map/MobileBottomSheet.tsx b/frontend/src/components/map/MobileBottomSheet.tsx
index e21bebd..1209335 100644
--- a/frontend/src/components/map/MobileBottomSheet.tsx
+++ b/frontend/src/components/map/MobileBottomSheet.tsx
@@ -54,10 +54,7 @@ function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
}
-export default function MobileBottomSheet({
- children,
- legend,
-}: MobileBottomSheetProps) {
+export default function MobileBottomSheet({ children, legend }: MobileBottomSheetProps) {
const viewport = useVisualViewportState();
const sheetRef = useRef
(null);
const scrollRef = useRef(null);
diff --git a/frontend/src/hooks/useMapData.test.ts b/frontend/src/hooks/useMapData.test.ts
index 5ea676b..e15db23 100644
--- a/frontend/src/hooks/useMapData.test.ts
+++ b/frontend/src/hooks/useMapData.test.ts
@@ -64,9 +64,7 @@ describe('useMapData', () => {
);
await act(async () => {
- result.current.handleViewChange(
- viewChange({ south: 1, west: 1, north: 2, east: 2 })
- );
+ result.current.handleViewChange(viewChange({ south: 1, west: 1, north: 2, east: 2 }));
});
await act(async () => {
vi.advanceTimersByTime(150);
@@ -74,9 +72,7 @@ describe('useMapData', () => {
expect(requests).toHaveLength(1);
await act(async () => {
- result.current.handleViewChange(
- viewChange({ south: 3, west: 3, north: 4, east: 4 })
- );
+ result.current.handleViewChange(viewChange({ south: 3, west: 3, north: 4, east: 4 }));
});
await act(async () => {
diff --git a/frontend/src/hooks/usePoiLayers.test.ts b/frontend/src/hooks/usePoiLayers.test.ts
index 96aa10d..697763f 100644
--- a/frontend/src/hooks/usePoiLayers.test.ts
+++ b/frontend/src/hooks/usePoiLayers.test.ts
@@ -34,6 +34,17 @@ const busStop: POI = {
emoji: 'đ',
};
+const foodWarehouse: POI = {
+ id: 'poi-4',
+ name: 'Iceland Avonmead Food Warehouse',
+ category: 'Iceland',
+ icon_category: 'The Food Warehouse',
+ group: 'Groceries',
+ lat: 51.49,
+ lng: -0.18,
+ emoji: 'đ',
+};
+
function layerById(layers: readonly unknown[], id: string) {
const layer = layers.find((item) => (item as { id?: string }).id === id);
if (!layer) throw new Error(`Layer ${id} not found`);
@@ -62,8 +73,18 @@ describe('usePoiLayers', () => {
const iconLayer = layerById(result.current.poiLayers, 'poi-icons');
const getIcon = iconLayer.props.getIcon as (poi: POI) => { url: string };
- expect(getIcon(waitrose).url).toBe(
- 'https://geolytix.github.io/MapIcons/brands/waitrose_24px.svg'
+ expect(getIcon(waitrose).url).toBe('/assets/poi-icons/brands/waitrose_24px.svg');
+ });
+
+ it('prefers POI fascia icon categories for map marker icons', () => {
+ const { result } = renderHook(() =>
+ usePoiLayers({ pois: [foodWarehouse], zoom: 15, isDark: false })
+ );
+ const iconLayer = layerById(result.current.poiLayers, 'poi-icons');
+ const getIcon = iconLayer.props.getIcon as (poi: POI) => { url: string };
+
+ expect(getIcon(foodWarehouse).url).toBe(
+ '/assets/poi-icons/brands/iceland_food_warehouse_24px.svg'
);
});
@@ -99,6 +120,7 @@ describe('usePoiLayers', () => {
y: 88,
name: supermarket.name,
category: supermarket.category,
+ icon_category: undefined,
group: supermarket.group,
emoji: supermarket.emoji,
id: supermarket.id,
diff --git a/frontend/src/hooks/usePoiLayers.ts b/frontend/src/hooks/usePoiLayers.ts
index 9c823fa..955d995 100644
--- a/frontend/src/hooks/usePoiLayers.ts
+++ b/frontend/src/hooks/usePoiLayers.ts
@@ -19,6 +19,7 @@ export interface PopupInfo {
y: number;
name: string;
category: string;
+ icon_category?: string;
group: string;
emoji: string;
id: string;
@@ -49,6 +50,7 @@ export function usePoiLayers({ pois, zoom, isDark }: UsePoiLayersProps) {
y: info.y,
name: info.object.name,
category: info.object.category,
+ icon_category: info.object.icon_category,
group: info.object.group,
emoji: info.object.emoji,
id: info.object.id,
@@ -176,7 +178,7 @@ export function usePoiLayers({ pois, zoom, isDark }: UsePoiLayersProps) {
data: visiblePois,
getPosition: (d) => [d.lng, d.lat],
getIcon: (d) => ({
- url: getPoiIconUrl(d.category, d.emoji),
+ url: getPoiIconUrl(d.category, d.emoji, d.icon_category, d.name),
width: 72,
height: 72,
}),
diff --git a/frontend/src/i18n/descriptions.ts b/frontend/src/i18n/descriptions.ts
index b934a20..b2e63ce 100644
--- a/frontend/src/i18n/descriptions.ts
+++ b/frontend/src/i18n/descriptions.ts
@@ -54,7 +54,8 @@ const descriptions: Record> = {
'Health Deprivation and Disability Score':
'Score de santé et handicap (plus élevé = meilleurs résultats)',
'Housing Conditions Score': 'Qualité et état du logement (plus élevé = meilleur)',
- 'Air Quality and Road Safety Score': 'QualitĂ© de lâair et sĂ©curitĂ© routiĂšre (plus Ă©levĂ© = meilleur)',
+ 'Air Quality and Road Safety Score':
+ 'QualitĂ© de lâair et sĂ©curitĂ© routiĂšre (plus Ă©levĂ© = meilleur)',
'Serious crime per 1k residents (avg/yr)': 'Taux de crimes graves pour 1 000 habitants par an',
'Minor crime per 1k residents (avg/yr)': 'Taux de délits mineurs pour 1 000 habitants par an',
'Serious crime (avg/yr)': 'Agrégat des catégories de crimes graves par an',
@@ -137,8 +138,7 @@ const descriptions: Record> = {
'Outstanding secondary schools within 5km':
'Von Ofsted mit Hervorragend bewertete weiterfĂŒhrende Schulen im Umkreis von 5 km',
'Education, Skills and Training Score': 'BildungsqualitÀtsscore der Gegend (höher = besser)',
- 'Income Score':
- 'Einkommensbenachteiligungsrate, invertiert (höher = weniger benachteiligt)',
+ 'Income Score': 'Einkommensbenachteiligungsrate, invertiert (höher = weniger benachteiligt)',
'Employment Score':
'BeschÀftigungsbenachteiligungsrate, invertiert (höher = weniger benachteiligt)',
'Health Deprivation and Disability Score':
diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts
index 30f7965..a76e362 100644
--- a/frontend/src/i18n/locales/de.ts
+++ b/frontend/src/i18n/locales/de.ts
@@ -433,8 +433,7 @@ const de: Translations = {
'Lebenslanger Zugang zu der Karte, die zeigt, wo Sie suchen sollten, bevor Sie Besichtigungen buchen.',
costContext:
'KĂ€ufer verbringen oft Abende damit, Inserate, Pendelzeiten, Schulberichte, KriminalitĂ€tskarten, Street View und Verkaufspreise zusammenzufĂŒhren. In London ist das besonders mĂŒhsam, aber dasselbe Rechercheproblem gibt es in ganz England. Perfect Postcode bringt die Gebietsrecherche auf eine Karte, bevor Sie Wochenenden, GebĂŒhren und Aufmerksamkeit investieren.',
- lessThanSurvey:
- 'Weniger als ein Gutachten. Deutlich wirksamer, um Ihre Auswahl zu steuern.',
+ lessThanSurvey: 'Weniger als ein Gutachten. Deutlich wirksamer, um Ihre Auswahl zu steuern.',
currentTier: 'Aktuelle Stufe',
firstNUsers: 'Erste {{count}} Nutzer',
everyoneAfter: 'Alle danach',
@@ -723,8 +722,7 @@ const de: Translations = {
genericFreeInvite: 'Du wurdest eingeladen, kostenlosen lebenslangen Zugang zu erhalten.',
genericDiscount: 'Ein Freund hat 30% Rabatt auf lebenslangen Zugang mit dir geteilt.',
exploreEvery: 'Finde Postleitzahlen, die zu deinem Leben passen',
- propertyInfo:
- 'Preise, Pendelzeit, Schulen, KriminalitÀt, LÀrm, Breitband, EPC und mehr',
+ propertyInfo: 'Preise, Pendelzeit, Schulen, KriminalitÀt, LÀrm, Breitband, EPC und mehr',
invalidInvite: 'UngĂŒltige Einladung',
inviteAlreadyUsed: 'Einladung bereits verwendet',
inviteAlreadyUsedDesc: 'Dieser Einladungslink wurde bereits eingelöst.',
diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts
index 84bb742..1a7ec11 100644
--- a/frontend/src/i18n/locales/fr.ts
+++ b/frontend/src/i18n/locales/fr.ts
@@ -725,8 +725,7 @@ const fr: Translations = {
genericFreeInvite: 'Vous avez été invité à obtenir un accÚs à vie gratuit.',
genericDiscount: 'Un ami vous fait bĂ©nĂ©ficier dâune rĂ©duction de 30% sur lâaccĂšs Ă vie.',
exploreEvery: 'Trouvez les codes postaux adaptés à votre vie',
- propertyInfo:
- 'Prix, trajet, écoles, criminalité, bruit, débit internet, DPE et plus encore',
+ propertyInfo: 'Prix, trajet, écoles, criminalité, bruit, débit internet, DPE et plus encore',
invalidInvite: 'Invitation invalide',
inviteAlreadyUsed: 'Invitation déjà utilisée',
inviteAlreadyUsedDesc: 'Ce lien dâinvitation a dĂ©jĂ Ă©tĂ© utilisĂ©.',
diff --git a/frontend/src/i18n/locales/hu.ts b/frontend/src/i18n/locales/hu.ts
index f3f26ff..f592d7d 100644
--- a/frontend/src/i18n/locales/hu.ts
+++ b/frontend/src/i18n/locales/hu.ts
@@ -589,15 +589,15 @@ const hu: Translations = {
faqSafety2A:
'FoglalĂĄs elĆtt ellenĆrizd a bƱnözĂ©st, közĂști zajt, internetet, parkokat, Ă©lelmiszerboltokat, iskolĂĄkat Ă©s ingĂĄzĂĄst. A hirdetĂ©si fotĂłk hasznosak lehetnek, de ne azokbĂłl derĂŒljön ki elĆször, milyen az utca.',
// FAQ items â Families and Schools
- faqFamilies1Q:
- 'Mely terĂŒleteken jĂł az iskolĂĄk, tĂ©r, biztonsĂĄg Ă©s ingĂĄzĂĄs keverĂ©ke?',
+ faqFamilies1Q: 'Mely terĂŒleteken jĂł az iskolĂĄk, tĂ©r, biztonsĂĄg Ă©s ingĂĄzĂĄs keverĂ©ke?',
faqFamilies1A:
'Tedd egy tĂ©rkĂ©pre az iskolaminĆsĂtĂ©seket, bƱnözĂ©st, parkokat, ingĂĄzĂĄst, teret, otthontĂpust Ă©s költsĂ©gvetĂ©st. Az eredmĂ©ny gyakorlati csalĂĄdi lista, nem sok kĂŒlön keresĂ©s halmaza.',
faqFamilies2Q: 'Ez bizonyĂtja, hogy iskola-felvĂ©teli körzeten belĂŒl vagyok?',
faqFamilies2A:
'Nem. Közeli iskolaminĆsĂ©get Ă©s helyi oktatĂĄsi informĂĄciĂłkat mutatunk, de a felvĂ©teli hatĂĄrok Ă©s elsĆbbsĂ©gi szabĂĄlyok vĂĄltozhatnak. A Perfect Postcode-dal vĂĄlogass helyeket, majd ellenĆrizd a körzeteket Ă©s felvĂ©telit az iskolĂĄnĂĄl vagy a helyi önkormĂĄnyzatnĂĄl.',
// FAQ items â Environment and Quality of Life
- faqEnv1Q: 'Hogyan kerĂŒlhetek el zajos utat az ingĂĄzĂĄs vagy internet minĆsĂ©gĂ©nek elvesztĂ©se nĂ©lkĂŒl?',
+ faqEnv1Q:
+ 'Hogyan kerĂŒlhetek el zajos utat az ingĂĄzĂĄs vagy internet minĆsĂ©gĂ©nek elvesztĂ©se nĂ©lkĂŒl?',
faqEnv1A:
'SzƱrj közĂști zajra, miközben az ingĂĄzĂĄs, internet, ĂĄr Ă©s otthonszƱrĆk aktĂvak maradnak. Egy jellemzĆ szerint szĂnezheted a tĂ©rkĂ©pet, a többi pedig reĂĄlisan tartja a listĂĄt.',
faqEnv2Q: 'Mutat ĂĄrvĂz-, sĂŒllyedĂ©s- vagy felmĂ©rĂ©si kockĂĄzatot?',
@@ -720,8 +720,7 @@ const hu: Translations = {
genericDiscount:
'Egy baråt megoszt veled egy 30%-os kedvezményt az élethosszig tartó hozzåférésre.',
exploreEvery: 'TalĂĄld meg az Ă©letedhez illĆ irĂĄnyĂtĂłszĂĄmokat',
- propertyInfo:
- 'Ărak, ingĂĄzĂĄs, iskolĂĄk, bƱnözĂ©s, zaj, szĂ©lessĂĄv, EPC Ă©s mĂ©g sok mĂĄs',
+ propertyInfo: 'Ărak, ingĂĄzĂĄs, iskolĂĄk, bƱnözĂ©s, zaj, szĂ©lessĂĄv, EPC Ă©s mĂ©g sok mĂĄs',
invalidInvite: 'ĂrvĂ©nytelen meghĂvĂł',
inviteAlreadyUsed: 'A meghĂvĂł mĂĄr felhasznĂĄlva',
inviteAlreadyUsedDesc: 'Ez a meghĂvĂł link mĂĄr be lett vĂĄltva.',
diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts
index 1be893e..2060054 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -346,8 +346,7 @@ const zh: Translations = {
showcaseFeatureTravelShort: 'ćșèĄ',
showcaseStep1Tab: 'çé',
showcaseStep1Title: 'ææšĄçłéæ±ćæçČŸćæçŽą',
- showcaseStep1Body:
- 'èźŸçœźçæŁéèŠçæĄä»¶ïŒćč¶æž
æ„çć°æŻéĄčèŠæ±äžșæšæé€äșć€ć°äžćéçéźçŒă',
+ showcaseStep1Body: 'èźŸçœźçæŁéèŠçæĄä»¶ïŒćč¶æž
æ„çć°æŻéĄčèŠæ±äžșæšæé€äșć€ć°äžćéçéźçŒă',
showcaseStep1Chip1: 'ćźéèĄé',
showcaseStep1Chip2: '饶çș§ć°ćŠ',
showcaseStep1Chip3: 'ÂŁ500k 仄ć
',
diff --git a/frontend/src/lib/consts.ts b/frontend/src/lib/consts.ts
index 717dd34..cb7aa5d 100644
--- a/frontend/src/lib/consts.ts
+++ b/frontend/src/lib/consts.ts
@@ -133,56 +133,59 @@ export const POI_DEFAULT_COLOR: [number, number, number] = [107, 114, 128];
/** POI category â icon/logo URL for branded and transport categories */
export const POI_CATEGORY_LOGOS: Record = {
Airport: '/assets/twemoji/2708.png',
- Aldi: 'https://geolytix.github.io/MapIcons/brands/aldi_24px.svg',
- Amazon: 'https://geolytix.github.io/MapIcons/brands/amazon_fresh_alt_24px.svg',
- Asda: 'https://geolytix.github.io/MapIcons/asda/asda_primary.svg',
- 'Asda Express': 'https://geolytix.github.io/MapIcons/asda/asda_express_24px.svg',
- 'Asda Living': 'https://geolytix.github.io/MapIcons/asda/asda_living_24px.svg',
- 'Asda PFS': 'https://geolytix.github.io/MapIcons/asda/asda_pfs_24px.svg',
+ Aldi: '/assets/poi-icons/logos/aldi.svg',
+ Amazon: '/assets/poi-icons/brands_2024/amazon_fresh.svg',
+ Asda: '/assets/poi-icons/logos/asda.svg',
+ 'Asda Express': '/assets/poi-icons/logos/asda.svg',
+ 'Asda Living': '/assets/poi-icons/logos/asda.svg',
+ 'Asda PFS': '/assets/poi-icons/logos/asda.svg',
+ 'Asda Supercentre': '/assets/poi-icons/logos/asda.svg',
+ 'Asda Supermarket': '/assets/poi-icons/logos/asda.svg',
+ 'Asda Superstore': '/assets/poi-icons/logos/asda.svg',
Bakery: '/assets/twemoji/1f950.png',
- Booths: 'https://geolytix.github.io/MapIcons/brands/booths_24px.svg',
- Budgens: 'https://geolytix.github.io/MapIcons/brands/budgens_24px.svg',
+ Booths: '/assets/poi-icons/brands_2024/booths.svg',
+ Budgens: '/assets/poi-icons/brands_2024/budgens.svg',
'Bus station': '/assets/twemoji/1f68c.png',
'Bus stop': '/assets/twemoji/1f68f.png',
'Butcher & Fishmonger': '/assets/twemoji/1f969.png',
- Centra: 'https://geolytix.github.io/MapIcons/brands/centra_24px.svg',
- 'Co-op': 'https://geolytix.github.io/MapIcons/brands/coop_24px.svg',
- COOK: 'https://geolytix.github.io/MapIcons/brands/cook.svg',
+ Centra: '/assets/poi-icons/logos/centra.svg',
+ 'Co-op': '/assets/poi-icons/logos/coop.svg',
+ COOK: '/assets/poi-icons/brands_2024/cook.svg',
'Convenience Store': '/assets/twemoji/1f3ea.png',
- Costco: 'https://geolytix.github.io/MapIcons/brands/costco_24px.svg',
+ Costco: '/assets/poi-icons/brands/costco.svg',
'Deli & Specialty': '/assets/twemoji/1f9c6.png',
- 'Dunnes Stores': 'https://geolytix.github.io/MapIcons/brands/dunnes_stores_24px.svg',
- Farmfoods: 'https://geolytix.github.io/MapIcons/brands/farmfoods_updated_24px.svg',
+ 'Dunnes Stores': '/assets/poi-icons/brands_2024/dunnes_stores.svg',
+ Farmfoods: '/assets/poi-icons/brands_2023/supermarkets/farmfoods.svg',
Ferry: '/assets/twemoji/26f4.png',
Greengrocer: '/assets/twemoji/1f96c.png',
- 'Heron Foods': 'https://geolytix.github.io/MapIcons/brands/heron_24px.svg',
- Iceland: 'https://geolytix.github.io/MapIcons/brands/iceland_24px.svg',
- Lidl: 'https://geolytix.github.io/MapIcons/brands/lidl_24px.svg',
- Makro: 'https://geolytix.github.io/MapIcons/brands/makro_24px.svg',
- 'M&S': 'https://geolytix.github.io/MapIcons/brands/mns_24px.svg',
- 'M&S Clothing': 'https://geolytix.github.io/MapIcons/brands/mns_high_street_24px.svg',
- 'M&S Food': 'https://geolytix.github.io/MapIcons/brands/mns_food_24px.svg',
- 'M&S Hospital': 'https://geolytix.github.io/MapIcons/brands/mns_hospital_24px.svg',
- 'M&S MSA': 'https://geolytix.github.io/MapIcons/brands/mns_moto_24px.svg',
- 'M&S Outlet': 'https://geolytix.github.io/MapIcons/brands/mns_outlet_24px.svg',
- Morrisons: 'https://geolytix.github.io/MapIcons/brands/morrisons_24px.svg',
- 'Morrisons Daily': 'https://geolytix.github.io/MapIcons/brands/morrisons_daily_24px.svg',
+ 'Heron Foods': '/assets/poi-icons/brands_2023/supermarkets/heron_foods.svg',
+ Iceland: '/assets/poi-icons/logos/iceland.svg',
+ Lidl: '/assets/poi-icons/logos/lidl.svg',
+ Makro: '/assets/poi-icons/brands_2024/makro.svg',
+ 'M&S': '/assets/poi-icons/brands/mns.svg',
+ 'M&S Clothing': '/assets/poi-icons/brands/mns_high_street.svg',
+ 'M&S Food': '/assets/poi-icons/brands/mns_food.svg',
+ 'M&S Hospital': '/assets/poi-icons/brands/mns_hospital.svg',
+ 'M&S MSA': '/assets/poi-icons/brands/mns_moto.svg',
+ 'M&S Outlet': '/assets/poi-icons/brands/mns_outlet.svg',
+ Morrisons: '/assets/poi-icons/logos/morrisons.svg',
+ 'Morrisons Daily': '/assets/poi-icons/brands_2024/morrisons_daily.svg',
'Off-Licence': '/assets/twemoji/1f377.png',
- 'Planet Organic': 'https://geolytix.github.io/MapIcons/logos/planet_organic_24px.svg',
+ 'Planet Organic': '/assets/poi-icons/logos/planet_organic.svg',
'Rail station': '/assets/twemoji/1f686.png',
- "Sainsbury's": 'https://geolytix.github.io/MapIcons/brands/sainsburys_24px.svg',
- "Sainsbury's Local": 'https://geolytix.github.io/MapIcons/brands/sainsburys_local_24px.svg',
- Spar: 'https://geolytix.github.io/MapIcons/brands/spar_24px.svg',
+ "Sainsbury's": '/assets/poi-icons/logos/sainsburys.svg',
+ "Sainsbury's Local": '/assets/poi-icons/brands_2024/sainsburys_local.svg',
+ Spar: '/assets/poi-icons/logos/spar.svg',
Supermarket: '/assets/twemoji/1f6d2.png',
- Tesco: 'https://geolytix.github.io/MapIcons/brands/tesco_24px.svg',
- 'Tesco Express': 'https://geolytix.github.io/MapIcons/brands/tesco_express_24px.svg',
- 'Tesco Extra': 'https://geolytix.github.io/MapIcons/brands/tesco_extra_24px.svg',
+ Tesco: '/assets/poi-icons/logos/tesco.svg',
+ 'Tesco Express': '/assets/poi-icons/logos/tesco_express.svg',
+ 'Tesco Extra': '/assets/poi-icons/logos/tesco_extra.svg',
'Taxi rank': '/assets/twemoji/1f695.png',
- 'The Food Warehouse': 'https://geolytix.github.io/MapIcons/brands/iceland_food_warehouse_24px.svg',
- 'Tube station': 'https://geolytix.github.io/MapIcons/public_transport/london_tube.svg',
- Waitrose: 'https://geolytix.github.io/MapIcons/brands/waitrose_24px.svg',
- 'Little Waitrose': 'https://geolytix.github.io/MapIcons/brands/little_waitrose_24px.svg',
- 'Whole Foods Market': 'https://geolytix.github.io/MapIcons/brands/wholefoods_24px.svg',
+ 'The Food Warehouse': '/assets/poi-icons/logos/iceland.svg',
+ 'Tube station': '/assets/poi-icons/public_transport/london_tube.svg',
+ Waitrose: '/assets/poi-icons/logos/waitrose.svg',
+ 'Little Waitrose': '/assets/poi-icons/brands/little_waitrose.svg',
+ 'Whole Foods Market': '/assets/poi-icons/brands_2024/wholefoods.svg',
};
/** Categories only shown when zoomed in past MINOR_POI_ZOOM_THRESHOLD */
diff --git a/frontend/src/lib/h3-selection.ts b/frontend/src/lib/h3-selection.ts
index f4291b5..4671d7a 100644
--- a/frontend/src/lib/h3-selection.ts
+++ b/frontend/src/lib/h3-selection.ts
@@ -62,8 +62,7 @@ function pointInPolygon(point: Point, polygon: Point[]): boolean {
if (current[1] > point[1] !== previous[1] > point[1]) {
const x =
- ((previous[0] - current[0]) * (point[1] - current[1])) /
- (previous[1] - current[1]) +
+ ((previous[0] - current[0]) * (point[1] - current[1])) / (previous[1] - current[1]) +
current[0];
if (point[0] < x) inside = !inside;
}
@@ -92,9 +91,7 @@ export function hasMatchingHexagonAtResolution(
hexagons: HexagonData[],
resolution: number
): boolean {
- return hexagons.some(
- (hexagon) => hexagon.count > 0 && getResolution(hexagon.h3) === resolution
- );
+ return hexagons.some((hexagon) => hexagon.count > 0 && getResolution(hexagon.h3) === resolution);
}
export function findOverlappingMatchingHexagon(
diff --git a/frontend/src/lib/map-utils.test.ts b/frontend/src/lib/map-utils.test.ts
index 44b922b..204a9ab 100644
--- a/frontend/src/lib/map-utils.test.ts
+++ b/frontend/src/lib/map-utils.test.ts
@@ -1,9 +1,12 @@
import { describe, expect, it } from 'vitest';
+import { existsSync } from 'fs';
+import { join } from 'path';
import {
DENSITY_GRADIENT,
ENUM_PALETTE,
FEATURE_GRADIENT,
+ POI_CATEGORY_LOGOS,
SMALLEST_VISIBLE_HEXAGON_RESOLUTION,
} from './consts';
import {
@@ -52,12 +55,25 @@ describe('map utilities', () => {
});
it('prefers POI category logos before falling back to emoji icons', () => {
- expect(getPoiIconUrl('Waitrose', 'đ')).toBe(
- 'https://geolytix.github.io/MapIcons/brands/waitrose_24px.svg'
+ expect(getPoiIconUrl('Waitrose', 'đ')).toBe('/assets/poi-icons/brands/waitrose_24px.svg');
+ expect(getPoiIconUrl('Iceland', 'đ', 'The Food Warehouse')).toBe(
+ '/assets/poi-icons/brands/iceland_food_warehouse_24px.svg'
+ );
+ expect(getPoiIconUrl("Sainsbury's", 'đ', undefined, 'Sainsburys Earlsfield Local')).toBe(
+ '/assets/poi-icons/brands/sainsburys_local_24px.svg'
);
expect(getPoiIconUrl('Unknown category', 'đ')).toBe('/assets/twemoji/1f6d2.png');
});
+ it('keeps POI icon URLs bundled locally', () => {
+ expect(Object.values(POI_CATEGORY_LOGOS).filter((url) => /^https?:\/\//.test(url))).toEqual([]);
+ expect(
+ Object.values(POI_CATEGORY_LOGOS)
+ .filter((url) => url.startsWith('/assets/poi-icons/'))
+ .filter((url) => !existsSync(join(process.cwd(), 'public', url.slice(1))))
+ ).toEqual([]);
+ });
+
it('returns fallback, filtered, enum, feature, and density colors', () => {
expect(
getFeatureFillColor(
diff --git a/frontend/src/lib/map-utils.ts b/frontend/src/lib/map-utils.ts
index 26b670e..51db978 100644
--- a/frontend/src/lib/map-utils.ts
+++ b/frontend/src/lib/map-utils.ts
@@ -241,7 +241,60 @@ export function emojiToTwemojiUrl(emoji: string): string {
return `${TWEMOJI_BASE}${hex}.png`;
}
-export function getPoiIconUrl(category: string, emoji: string): string {
+function inferPoiIconCategory(category: string, name?: string): string | undefined {
+ if (!name) return undefined;
+ const text = `${category} ${name}`.toLowerCase();
+
+ switch (category) {
+ case 'Asda':
+ if (text.includes('asda express') || text.includes(' express')) return 'Asda Express';
+ if (text.includes('asda living')) return 'Asda Living';
+ if (text.includes('asda pfs') || /\bpfs\b/.test(text)) return 'Asda PFS';
+ return undefined;
+ case 'Iceland':
+ return text.includes('food warehouse') ? 'The Food Warehouse' : undefined;
+ case 'M&S':
+ if (text.includes('hospital')) return 'M&S Hospital';
+ if (text.includes('moto')) return 'M&S MSA';
+ if (text.includes('outlet')) return 'M&S Outlet';
+ if (
+ text.includes('foodhall') ||
+ text.includes('simply food') ||
+ text.includes('food to go') ||
+ text.includes(' bp') ||
+ /\bsf\b/.test(text)
+ ) {
+ return 'M&S Food';
+ }
+ if (text.includes('clothing')) return 'M&S Clothing';
+ return undefined;
+ case 'Morrisons':
+ return text.includes('morrisons daily') || text.includes('morrisons dailly')
+ ? 'Morrisons Daily'
+ : undefined;
+ case "Sainsbury's":
+ return text.includes('local') ? "Sainsbury's Local" : undefined;
+ case 'Tesco':
+ if (text.includes('tesco extra')) return 'Tesco Extra';
+ if (text.includes('tesco express') || text.includes(' express')) return 'Tesco Express';
+ return undefined;
+ case 'Waitrose':
+ return text.includes('little waitrose') ? 'Little Waitrose' : undefined;
+ default:
+ return undefined;
+ }
+}
+
+export function getPoiIconUrl(
+ category: string,
+ emoji: string,
+ iconCategory?: string,
+ name?: string
+): string {
+ const resolvedIconCategory = iconCategory || inferPoiIconCategory(category, name);
+ if (resolvedIconCategory && POI_CATEGORY_LOGOS[resolvedIconCategory]) {
+ return POI_CATEGORY_LOGOS[resolvedIconCategory];
+ }
return POI_CATEGORY_LOGOS[category] ?? emojiToTwemojiUrl(emoji);
}
diff --git a/frontend/src/lib/url-state.ts b/frontend/src/lib/url-state.ts
index c83e25c..c201b83 100644
--- a/frontend/src/lib/url-state.ts
+++ b/frontend/src/lib/url-state.ts
@@ -8,7 +8,6 @@ import {
import {
SCHOOL_FILTER_NAME,
createSchoolFilterKey,
- getSchoolBackendFeatureName,
getSchoolFilterConfig,
isSchoolFilterName,
type SchoolDistance,
diff --git a/pipeline/check_travel_times.py b/pipeline/check_travel_times.py
index 561d310..79683e2 100644
--- a/pipeline/check_travel_times.py
+++ b/pipeline/check_travel_times.py
@@ -81,11 +81,7 @@ def find_bad_files(
bad: list[BadFile] = []
stats: dict[str, dict] = {}
- modes = sorted(
- d
- for d in os.listdir(base_dir)
- if (base_dir / d).is_dir()
- )
+ modes = sorted(d for d in os.listdir(base_dir) if (base_dir / d).is_dir())
for mode in modes:
mode_dir = base_dir / mode
@@ -149,7 +145,9 @@ def find_duplicates(base_dir: Path) -> tuple[list[BadFile], dict[str, dict]]:
# Keep the file with the most rows
files.sort(key=lambda x: x[1], reverse=True)
for filename, rows in files[1:]:
- dupes.append(BadFile(mode=mode, filename=filename, slug=slug, rows=rows))
+ dupes.append(
+ BadFile(mode=mode, filename=filename, slug=slug, rows=rows)
+ )
mode_dupes += 1
duped_slugs = sum(1 for fs in slug_files.values() if len(fs) > 1)
@@ -197,7 +195,9 @@ def main() -> None:
bad_files, stats = find_bad_files(args.travel_times, args.threshold_pct)
print("=== Per-mode summary ===\n")
- print(f"{'Mode':<10} {'Total':>6} {'Bad':>5} {'Threshold':>10} {'Median':>8} {'Range':>20}")
+ print(
+ f"{'Mode':<10} {'Total':>6} {'Bad':>5} {'Threshold':>10} {'Median':>8} {'Range':>20}"
+ )
print("-" * 65)
for mode, s in sorted(stats.items()):
rng = f"{s['min']:,}â{s['max']:,}"
@@ -231,7 +231,9 @@ def main() -> None:
total_removable = sum(s["removable"] for s in dupe_stats.values())
if total_removable > 0:
print(f"\n=== Duplicates ({total_removable} removable files) ===\n")
- print(f"{'Mode':<10} {'Total':>6} {'Unique':>7} {'Duped slugs':>12} {'Removable':>10}")
+ print(
+ f"{'Mode':<10} {'Total':>6} {'Unique':>7} {'Duped slugs':>12} {'Removable':>10}"
+ )
print("-" * 50)
for mode, s in sorted(dupe_stats.items()):
if s["removable"] > 0:
@@ -242,9 +244,15 @@ def main() -> None:
if args.dedup:
# Exclude files already deleted by --delete
- deleted_set = {(bf.mode, bf.filename) for bf in bad_files} if args.delete else set()
- to_delete = [df for df in dupe_files if (df.mode, df.filename) not in deleted_set]
- print(f"\nRemoving {len(to_delete)} duplicate files (keeping largest per slug)...")
+ deleted_set = (
+ {(bf.mode, bf.filename) for bf in bad_files} if args.delete else set()
+ )
+ to_delete = [
+ df for df in dupe_files if (df.mode, df.filename) not in deleted_set
+ ]
+ print(
+ f"\nRemoving {len(to_delete)} duplicate files (keeping largest per slug)..."
+ )
deleted = _delete_files(args.travel_times, to_delete)
print(f"Deleted {deleted}/{len(to_delete)} files.")
else:
diff --git a/pipeline/download/geolytix_retail_points.py b/pipeline/download/geolytix_retail_points.py
index 97ee72d..3520105 100644
--- a/pipeline/download/geolytix_retail_points.py
+++ b/pipeline/download/geolytix_retail_points.py
@@ -42,9 +42,7 @@ def select_latest_csv_name(names: list[str]) -> str:
match = CSV_NAME_RE.match(path.name)
if not match:
continue
- candidates.append(
- (match.group("release"), int(match.group("version")), name)
- )
+ candidates.append((match.group("release"), int(match.group("version")), name))
if not candidates:
raise ValueError("No root-level GEOLYTIX retail points CSV found")
diff --git a/pipeline/download/map_assets.py b/pipeline/download/map_assets.py
index 5bc8dae..1de7d84 100644
--- a/pipeline/download/map_assets.py
+++ b/pipeline/download/map_assets.py
@@ -9,6 +9,7 @@ from pipeline.transform.transform_poi import NAPTAN_EMOJIS, _CATEGORIES
GLYPHS_BASE = "https://protomaps.github.io/basemaps-assets/fonts"
SPRITES_BASE = "https://protomaps.github.io/basemaps-assets/sprites/v4"
TWEMOJI_BASE = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72"
+POI_ICON_BASE = "https://geolytix.github.io/MapIcons"
# Font stacks used by @protomaps/basemaps with lang='en'
FONT_STACKS = ["Noto Sans Regular", "Noto Sans Italic", "Noto Sans Medium"]
@@ -16,6 +17,50 @@ FONT_STACKS = ["Noto Sans Regular", "Noto Sans Italic", "Noto Sans Medium"]
# Fallback emoji not in any category
_FALLBACK_EMOJIS = ["đ"]
+POI_ICON_PATHS = [
+ "asda/asda_express_24px.svg",
+ "asda/asda_green_basket_24px.svg",
+ "asda/asda_green_trolley_24px.svg",
+ "asda/asda_living_24px.svg",
+ "asda/asda_pfs_24px.svg",
+ "asda/asda_primary.svg",
+ "asda/asda_superstore_green_trolley_24px.svg",
+ "brands/aldi_24px.svg",
+ "brands/amazon_fresh_alt_24px.svg",
+ "brands/booths_24px.svg",
+ "brands/budgens_24px.svg",
+ "brands/centra_24px.svg",
+ "brands/cook.svg",
+ "brands/coop_24px.svg",
+ "brands/costco_24px.svg",
+ "brands/dunnes_stores_24px.svg",
+ "brands/farmfoods_updated_24px.svg",
+ "brands/heron_24px.svg",
+ "brands/iceland_24px.svg",
+ "brands/iceland_food_warehouse_24px.svg",
+ "brands/lidl_24px.svg",
+ "brands/little_waitrose_24px.svg",
+ "brands/makro_24px.svg",
+ "brands/mns_24px.svg",
+ "brands/mns_food_24px.svg",
+ "brands/mns_high_street_24px.svg",
+ "brands/mns_hospital_24px.svg",
+ "brands/mns_moto_24px.svg",
+ "brands/mns_outlet_24px.svg",
+ "brands/morrisons_24px.svg",
+ "brands/morrisons_daily_24px.svg",
+ "brands/sainsburys_24px.svg",
+ "brands/sainsburys_local_24px.svg",
+ "brands/spar_24px.svg",
+ "brands/tesco_24px.svg",
+ "brands/tesco_express_24px.svg",
+ "brands/tesco_extra_24px.svg",
+ "brands/waitrose_24px.svg",
+ "brands/wholefoods_24px.svg",
+ "logos/planet_organic_24px.svg",
+ "public_transport/london_tube.svg",
+]
+
def collect_twemoji_codes() -> list[str]:
"""Derive twemoji hex codes from transform_poi categories.
@@ -93,6 +138,12 @@ def main():
url = f"{TWEMOJI_BASE}/{code}.png"
tasks.append((url, twemoji_dir / f"{code}.png"))
+ # Branded POI icons are served from this local bundle at runtime.
+ poi_icons_dir = out / "poi-icons"
+ for icon_path in POI_ICON_PATHS:
+ url = f"{POI_ICON_BASE}/{icon_path}"
+ tasks.append((url, poi_icons_dir / icon_path))
+
# Skip already-downloaded files
remaining = [(url, dest) for url, dest in tasks]
diff --git a/pipeline/download/median_age.py b/pipeline/download/median_age.py
index 1e262b1..4471ce8 100644
--- a/pipeline/download/median_age.py
+++ b/pipeline/download/median_age.py
@@ -23,24 +23,24 @@ PAGE_SIZE = 25000
# Five-year age bands in order, with lower bounds for interpolation.
# The last band (85+) is open-ended â we treat it as 85-89 for median purposes.
AGE_BANDS = [
- (0, 5), # Aged 0 to 4 years
- (5, 5), # Aged 5 to 9 years
- (10, 5), # Aged 10 to 14 years
- (15, 5), # Aged 15 to 19 years
- (20, 5), # Aged 20 to 24 years
- (25, 5), # Aged 25 to 29 years
- (30, 5), # Aged 30 to 34 years
- (35, 5), # Aged 35 to 39 years
- (40, 5), # Aged 40 to 44 years
- (45, 5), # Aged 45 to 49 years
- (50, 5), # Aged 50 to 54 years
- (55, 5), # Aged 55 to 59 years
- (60, 5), # Aged 60 to 64 years
- (65, 5), # Aged 65 to 69 years
- (70, 5), # Aged 70 to 74 years
- (75, 5), # Aged 75 to 79 years
- (80, 5), # Aged 80 to 84 years
- (85, 5), # Aged 85 years and over
+ (0, 5), # Aged 0 to 4 years
+ (5, 5), # Aged 5 to 9 years
+ (10, 5), # Aged 10 to 14 years
+ (15, 5), # Aged 15 to 19 years
+ (20, 5), # Aged 20 to 24 years
+ (25, 5), # Aged 25 to 29 years
+ (30, 5), # Aged 30 to 34 years
+ (35, 5), # Aged 35 to 39 years
+ (40, 5), # Aged 40 to 44 years
+ (45, 5), # Aged 45 to 49 years
+ (50, 5), # Aged 50 to 54 years
+ (55, 5), # Aged 55 to 59 years
+ (60, 5), # Aged 60 to 64 years
+ (65, 5), # Aged 65 to 69 years
+ (70, 5), # Aged 70 to 74 years
+ (75, 5), # Aged 75 to 79 years
+ (80, 5), # Aged 80 to 84 years
+ (85, 5), # Aged 85 years and over
]
@@ -110,14 +110,18 @@ def download_and_convert(output_path: Path) -> None:
for row in rows:
counts = [row[col] for col in band_cols]
median = compute_median_age(counts)
- medians.append({"lsoa21": row["GEOGRAPHY_CODE"], "median_age": round(median, 1)})
+ medians.append(
+ {"lsoa21": row["GEOGRAPHY_CODE"], "median_age": round(median, 1)}
+ )
result = pl.DataFrame(medians).with_columns(
pl.col("median_age").cast(pl.Float32),
)
print(f"England LSOAs: {result.height}")
- print(f"Median age range: {result['median_age'].min()} - {result['median_age'].max()}")
+ print(
+ f"Median age range: {result['median_age'].min()} - {result['median_age'].max()}"
+ )
print(f"Mean of medians: {result['median_age'].mean():.1f}")
output_path.parent.mkdir(parents=True, exist_ok=True)
diff --git a/pipeline/download/rental_prices.py b/pipeline/download/rental_prices.py
index e44c82e..ccb6735 100644
--- a/pipeline/download/rental_prices.py
+++ b/pipeline/download/rental_prices.py
@@ -43,9 +43,7 @@ def convert_to_parquet(xlsx_path: Path, parquet_path: Path) -> None:
# Filter to English local authorities
df = df.filter(
- pl.any_horizontal(
- pl.col("area_code").str.starts_with(p) for p in LA_PREFIXES
- )
+ pl.any_horizontal(pl.col("area_code").str.starts_with(p) for p in LA_PREFIXES)
)
# Use only the latest month
diff --git a/pipeline/download/tiles.py b/pipeline/download/tiles.py
index 1fece9c..a8c0b2f 100644
--- a/pipeline/download/tiles.py
+++ b/pipeline/download/tiles.py
@@ -23,7 +23,9 @@ def find_latest_build() -> str:
for i in range(MAX_AGE_DAYS):
d = today - timedelta(days=i)
url = f"{PROTOMAPS_BASE}/{d:%Y%m%d}.pmtiles"
- req = urllib.request.Request(url, method="HEAD", headers={"User-Agent": USER_AGENT})
+ req = urllib.request.Request(
+ url, method="HEAD", headers={"User-Agent": USER_AGENT}
+ )
try:
urllib.request.urlopen(req)
print(f"Found build: {d:%Y%m%d}")
diff --git a/pipeline/transform/join_epc_pp.py b/pipeline/transform/join_epc_pp.py
index f0aba76..2f90389 100644
--- a/pipeline/transform/join_epc_pp.py
+++ b/pipeline/transform/join_epc_pp.py
@@ -128,9 +128,7 @@ def main():
# Social tenure fork: flag properties that were ever social housing
social_tenure = (
- epc_base.filter(
- pl.col("TENURE").str.to_lowercase().str.contains("social")
- )
+ epc_base.filter(pl.col("TENURE").str.to_lowercase().str.contains("social"))
.select("epc_address", "POSTCODE")
.unique()
.with_columns(pl.lit("Yes").alias("was_council_house"))
@@ -139,16 +137,20 @@ def main():
print(f"Former council houses (EPC social tenure): {social_tenure.height}")
# Left-join events and social tenure back onto dedup EPC
- epc = epc.join(
- events.lazy(),
- on=["epc_address", "POSTCODE"],
- how="left",
- ).join(
- social_tenure.lazy(),
- on=["epc_address", "POSTCODE"],
- how="left",
- ).with_columns(
- pl.col("was_council_house").fill_null("No"),
+ epc = (
+ epc.join(
+ events.lazy(),
+ on=["epc_address", "POSTCODE"],
+ how="left",
+ )
+ .join(
+ social_tenure.lazy(),
+ on=["epc_address", "POSTCODE"],
+ how="left",
+ )
+ .with_columns(
+ pl.col("was_council_house").fill_null("No"),
+ )
)
print("EPC dataset")
diff --git a/pipeline/transform/transform_poi.py b/pipeline/transform/transform_poi.py
index 31f595d..831d6d2 100644
--- a/pipeline/transform/transform_poi.py
+++ b/pipeline/transform/transform_poi.py
@@ -1092,6 +1092,7 @@ GROCERY_FASCIA_ICON_NAMES: dict[str, str] = {
"Asda Living": "Asda Living",
"Asda PFS": "Asda PFS",
"Cooltrader": "Heron Foods",
+ "Co-op Food": "Co-op",
"Cook": "COOK",
"Eurospar": "Spar",
"Eurospar PFS": "Spar",
@@ -1144,9 +1145,7 @@ def transform_grocery_retail_points(
required = {"id", "retailer", "fascia", "store_name", "long_wgs", "lat_wgs"}
missing = required - set(grocery_df.columns)
if missing:
- raise ValueError(
- f"GEOLYTIX retail points missing columns: {sorted(missing)}"
- )
+ raise ValueError(f"GEOLYTIX retail points missing columns: {sorted(missing)}")
df = (
grocery_df.select(
diff --git a/server-rs/src/data/property.rs b/server-rs/src/data/property.rs
index e5cb254..612a021 100644
--- a/server-rs/src/data/property.rs
+++ b/server-rs/src/data/property.rs
@@ -383,6 +383,7 @@ fn token_matches_numeric_term(token: &str, query_term: &str) -> bool {
token == query_term || token.starts_with(query_term)
}
+#[cfg(test)]
fn address_tokens_match_group(tokens: &[String], group: &AddressTermGroup) -> bool {
group.alternatives.iter().any(|alternative| {
tokens
@@ -654,7 +655,7 @@ impl PropertyData {
for term in terms {
if let Some(rows) = self.address_token_index.get(term) {
- if best.map_or(true, |current| rows.len() < current.len()) {
+ if best.is_none_or(|current| rows.len() < current.len()) {
best = Some(rows.as_slice());
}
continue;
@@ -670,7 +671,7 @@ impl PropertyData {
continue;
}
if let Some(rows) = self.address_token_index.get(token) {
- if best.map_or(true, |current| rows.len() < current.len()) {
+ if best.is_none_or(|current| rows.len() < current.len()) {
best = Some(rows.as_slice());
}
}