diff --git a/src/hooks/useDeriveRegions.ts b/src/hooks/useDeriveRegions.ts index 7c8ea91..d7fbcae 100644 --- a/src/hooks/useDeriveRegions.ts +++ b/src/hooks/useDeriveRegions.ts @@ -64,11 +64,20 @@ export function useDeriveRegions(): DeriveRegionsState { running.current = true; void (async () => { + let lastRefresh = 0; try { - await fillDerivedRegions(misses); + await fillDerivedRegions(misses, (done, total) => { + setPending(Math.max(0, total - done)); + // Refresh the card as pins resolve (not just at the end) so the numbers + // climb while you watch, instead of jumping once. + if (done - lastRefresh >= 5) { + lastRefresh = done; + queryClient.invalidateQueries({ queryKey: ['growth-stats'] }); + } + }); const unresolved = misses.filter((c) => !getDerivedRegion(c.lat, c.lng)).length; setPending(unresolved); - // Recompute the region card now that more pins have a derived region. + // Final recompute now that every pin has (or has failed to get) a region. queryClient.invalidateQueries({ queryKey: ['growth-stats'] }); } finally { running.current = false; diff --git a/src/hooks/useGrowthStats.ts b/src/hooks/useGrowthStats.ts index 3b21c4a..830aa73 100644 --- a/src/hooks/useGrowthStats.ts +++ b/src/hooks/useGrowthStats.ts @@ -315,13 +315,23 @@ export function useGrowthStats() { return null; }; const regionEntries = allLocations.map((e) => regionOf(e)); + // A country can arrive WITH a code (tagged pins) or WITHOUT one (some + // geocodes). Learn name→code from whichever pins carry both, so e.g. + // "United Kingdom" with no code folds into the "GB" bucket instead of + // splitting into two identical rows. + const codeByName = new Map(); + for (const region of regionEntries) { + if (region?.country && region.code) codeByName.set(region.country.toLowerCase(), region.code); + } const regionGroups = (kind: 'country' | 'city') => { const groups = new Map }>(); for (const region of regionEntries) { if (!region) continue; const name = kind === 'country' ? region.country : region.city; if (!name) continue; - const key = kind === 'country' ? region.code || name.toLowerCase() : name.toLowerCase(); + const key = kind === 'country' + ? region.code || codeByName.get(name.toLowerCase()) || name.toLowerCase() + : name.toLowerCase(); const g = groups.get(key) ?? { total: 0, labels: new Map() }; g.total += 1; g.labels.set(name, (g.labels.get(name) ?? 0) + 1); diff --git a/src/lib/derivedRegions.ts b/src/lib/derivedRegions.ts index 8a7dc31..4466630 100644 --- a/src/lib/derivedRegions.ts +++ b/src/lib/derivedRegions.ts @@ -93,19 +93,26 @@ export function coordsOf(event: { tags: string[][] }): { lat: number; lng: numbe */ export async function fillDerivedRegions( coords: Array<{ lat: number; lng: number }>, + onProgress?: (done: number, total: number) => void, ): Promise { let filled = 0; + const seen = new Set(); for (let i = 0; i < coords.length; i += 1) { const { lat, lng } = coords[i]; - if (getDerivedRegion(lat, lng)) continue; - const geo = await reverseGeocodeRegions(lat, lng); - if (geo.city || geo.country || geo.countryCode) { - setDerivedRegion(lat, lng, { city: geo.city, country: geo.country, countryCode: geo.countryCode }); - filled += 1; - } - if (i < coords.length - 1) { - await new Promise((resolve) => setTimeout(resolve, GEOCODE_SPACING_MS)); + const key = regionCoordKey(lat, lng); + // Skip duplicates / already-cached pins, but still advance the progress bar. + if (!seen.has(key) && !getDerivedRegion(lat, lng)) { + seen.add(key); + const geo = await reverseGeocodeRegions(lat, lng); + if (geo.city || geo.country || geo.countryCode) { + setDerivedRegion(lat, lng, { city: geo.city, country: geo.country, countryCode: geo.countryCode }); + filled += 1; + } + if (i < coords.length - 1) { + await new Promise((resolve) => setTimeout(resolve, GEOCODE_SPACING_MS)); + } } + onProgress?.(i + 1, coords.length); } return filled; }