feat(atlas): sub-national region view when zooming in

- Zoom >= 5 shows visited regions (states/provinces/departments) colored on the map
- Server resolves places to regions via Nominatim reverse geocoding (zoom=8)
- Supports all ISO levels: lvl4 (states), lvl5 (provinces), lvl6 (departments)
- Handles city-states (Berlin, Vienna, Hamburg) via city/county fallback
- Fuzzy name matching between Nominatim and GeoJSON for cross-format compatibility
- 10m admin_1 GeoJSON loaded server-side (cached), filtered per country
- Region colors match their parent country color
- Custom DOM tooltip (ref-based, no re-renders on hover)
- Country layer dims to 35% opacity when regions visible
- place_regions DB table caches resolved regions permanently
- Rate-limited Nominatim calls (1 req/sec) with progressive resolution
This commit is contained in:
mauriceboe
2026-04-05 01:31:19 +02:00
parent b8058a2755
commit 16cadeb09e
4 changed files with 313 additions and 3 deletions
+17
View File
@@ -6,6 +6,8 @@ import {
getCountryPlaces,
markCountryVisited,
unmarkCountryVisited,
getVisitedRegions,
getRegionGeo,
listBucketList,
createBucketItem,
updateBucketItem,
@@ -21,6 +23,21 @@ router.get('/stats', async (req: Request, res: Response) => {
res.json(data);
});
router.get('/regions', async (req: Request, res: Response) => {
const userId = (req as AuthRequest).user.id;
res.setHeader('Cache-Control', 'no-cache, no-store');
const data = await getVisitedRegions(userId);
res.json(data);
});
router.get('/regions/geo', async (req: Request, res: Response) => {
const countries = (req.query.countries as string || '').split(',').filter(Boolean);
if (countries.length === 0) return res.json({ type: 'FeatureCollection', features: [] });
const geo = await getRegionGeo(countries);
res.setHeader('Cache-Control', 'public, max-age=86400');
res.json(geo);
});
router.get('/country/:code', (req: Request, res: Response) => {
const userId = (req as AuthRequest).user.id;
const code = req.params.code.toUpperCase();