fix(stats): tick the region-derivation counter live and merge name/code groups

Two issues from the first derived-regions pass:
- The 'Resolving N...' counter only updated when the whole pass finished, so it
  looked frozen. fillDerivedRegions now reports progress per pin, the hook ticks
  the counter down, and the card refreshes every 5 resolved pins so the numbers
  climb while you watch.
- A country could appear twice ('United Kingdom' 1 + 1) when one pin carried a
  country_code and another only a name. Learn name->code from pins that have
  both and fold name-only pins into the same bucket.
This commit is contained in:
SkateSpots 2026-10-01 14:22:02 +07:00
parent 7bad809878
commit 6091a0acd8
3 changed files with 37 additions and 11 deletions

View file

@ -64,11 +64,20 @@ export function useDeriveRegions(): DeriveRegionsState {
running.current = true; running.current = true;
void (async () => { void (async () => {
let lastRefresh = 0;
try { 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; const unresolved = misses.filter((c) => !getDerivedRegion(c.lat, c.lng)).length;
setPending(unresolved); 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'] }); queryClient.invalidateQueries({ queryKey: ['growth-stats'] });
} finally { } finally {
running.current = false; running.current = false;

View file

@ -315,13 +315,23 @@ export function useGrowthStats() {
return null; return null;
}; };
const regionEntries = allLocations.map((e) => regionOf(e)); 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<string, string>();
for (const region of regionEntries) {
if (region?.country && region.code) codeByName.set(region.country.toLowerCase(), region.code);
}
const regionGroups = (kind: 'country' | 'city') => { const regionGroups = (kind: 'country' | 'city') => {
const groups = new Map<string, { total: number; labels: Map<string, number> }>(); const groups = new Map<string, { total: number; labels: Map<string, number> }>();
for (const region of regionEntries) { for (const region of regionEntries) {
if (!region) continue; if (!region) continue;
const name = kind === 'country' ? region.country : region.city; const name = kind === 'country' ? region.country : region.city;
if (!name) continue; 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<string, number>() }; const g = groups.get(key) ?? { total: 0, labels: new Map<string, number>() };
g.total += 1; g.total += 1;
g.labels.set(name, (g.labels.get(name) ?? 0) + 1); g.labels.set(name, (g.labels.get(name) ?? 0) + 1);

View file

@ -93,11 +93,16 @@ export function coordsOf(event: { tags: string[][] }): { lat: number; lng: numbe
*/ */
export async function fillDerivedRegions( export async function fillDerivedRegions(
coords: Array<{ lat: number; lng: number }>, coords: Array<{ lat: number; lng: number }>,
onProgress?: (done: number, total: number) => void,
): Promise<number> { ): Promise<number> {
let filled = 0; let filled = 0;
const seen = new Set<string>();
for (let i = 0; i < coords.length; i += 1) { for (let i = 0; i < coords.length; i += 1) {
const { lat, lng } = coords[i]; const { lat, lng } = coords[i];
if (getDerivedRegion(lat, lng)) continue; 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); const geo = await reverseGeocodeRegions(lat, lng);
if (geo.city || geo.country || geo.countryCode) { if (geo.city || geo.country || geo.countryCode) {
setDerivedRegion(lat, lng, { city: geo.city, country: geo.country, countryCode: geo.countryCode }); setDerivedRegion(lat, lng, { city: geo.city, country: geo.country, countryCode: geo.countryCode });
@ -107,5 +112,7 @@ export async function fillDerivedRegions(
await new Promise((resolve) => setTimeout(resolve, GEOCODE_SPACING_MS)); await new Promise((resolve) => setTimeout(resolve, GEOCODE_SPACING_MS));
} }
} }
onProgress?.(i + 1, coords.length);
}
return filled; return filled;
} }