Fix Sasquatch spot results: honest count + collapse duplicates
Two bugs from "spots in bangkok" (said "Found 20" but showed 6, with repeats): 1. Misleading count: formatLocalResults hard-capped the visible list to 6 while the headline reported the full count. Now shows up to 10 and appends a truthful "…and N more — see them all on the map" line, so the number and the list always agree. 2. Duplicate spots: coordinate dedupe (kind:pubkey:d) can't catch the same real-world spot re-posted under a different identifier or by another account (e.g. "Yellow Park – Sukhumvit 50" appearing twice). Added deduplicateByIdentity() — a second-pass dedupe on a normalized name + geohash/location signature, keeping the freshest event — applied in findSpots so the count and list are clean. Both fixes flow through the AI tool-call path and the offline fallback path. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
ada4bfee5e
commit
8b7d9ca4ea
2 changed files with 44 additions and 2 deletions
|
|
@ -414,7 +414,11 @@ function formatLocalResults(intent: LocalIntent, result: unknown): string {
|
|||
return `No ${label} listed there yet 🦧 — be the first to add some at [skate-spots.org](https://skate-spots.org).`;
|
||||
}
|
||||
|
||||
const lines = items.slice(0, 6).map((it) => {
|
||||
// Show a generous slice, then honestly summarise the rest so the headline
|
||||
// count and the visible list never disagree.
|
||||
const MAX_SHOWN = 10;
|
||||
const shown = items.slice(0, MAX_SHOWN);
|
||||
const lines = shown.map((it) => {
|
||||
const name = (it.name ?? it.title ?? it.role ?? 'Unnamed') as string;
|
||||
const url = it.url as string | undefined;
|
||||
const sub = (it.location ?? it.city ?? it.start ?? '') as string;
|
||||
|
|
@ -429,7 +433,13 @@ function formatLocalResults(intent: LocalIntent, result: unknown): string {
|
|||
intent.tool === 'find_crews' ? '👥' :
|
||||
intent.tool === 'find_shops' ? '🏪' :
|
||||
intent.tool === 'find_tours' ? '🗺️' : '🛍️';
|
||||
return `${emoji} Found **${items.length}** ${label}:\n${lines}`;
|
||||
|
||||
const remaining = items.length - shown.length;
|
||||
const more = remaining > 0
|
||||
? `\n\n…and **${remaining}** more — [see them all on the map](https://skate-spots.org/map) 🗺️`
|
||||
: '';
|
||||
|
||||
return `${emoji} Found **${items.length}** ${label}:\n${lines}${more}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -182,6 +182,34 @@ function deduplicateAddressable(events: { pubkey: string; kind: number; tags: st
|
|||
return Array.from(seen.values());
|
||||
}
|
||||
|
||||
/** Normalise a string for fuzzy matching: lowercase, trim, collapse whitespace. */
|
||||
function normalizeForDedupe(s: string | undefined): string {
|
||||
return (s ?? '').toLowerCase().replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse spots that are the SAME real-world spot published as different
|
||||
* addressable events — e.g. the same place re-posted under a new `d` identifier
|
||||
* or by two different accounts. Coordinate dedupe (kind:pubkey:d) can't catch
|
||||
* these because the coordinates differ, so we also dedupe on a
|
||||
* name + location/geohash signature and keep the freshest event.
|
||||
*/
|
||||
function deduplicateByIdentity<T extends { tags: string[][]; created_at: number }>(spots: T[]): T[] {
|
||||
const seen = new Map<string, T>();
|
||||
for (const e of spots) {
|
||||
const name = normalizeForDedupe(getTag(e, 'title') ?? getTag(e, 'name'));
|
||||
// Prefer geohash (precise) then location text as the place signature.
|
||||
const place = normalizeForDedupe(getTag(e, 'g') ?? getTag(e, 'location') ?? getTag(e, 'city'));
|
||||
// No name → can't confidently match; keep it (unique key on coordinate).
|
||||
const key = name ? `${name}|${place}` : `__unique__${buildNaddr(e as unknown as { kind: number; pubkey: string; tags: string[][] })}`;
|
||||
const existing = seen.get(key);
|
||||
if (!existing || e.created_at > existing.created_at) {
|
||||
seen.set(key, e);
|
||||
}
|
||||
}
|
||||
return Array.from(seen.values());
|
||||
}
|
||||
|
||||
function buildNaddr(event: { kind: number; pubkey: string; tags: string[][] }): string {
|
||||
return `${event.kind}:${event.pubkey}:${getTag(event, 'd') ?? ''}`;
|
||||
}
|
||||
|
|
@ -215,6 +243,10 @@ export function useCVMClient() {
|
|||
e.tags.some((t) => t[0] === 't' && t[1] === 'skatespot')
|
||||
);
|
||||
|
||||
// Collapse the same real-world spot re-posted under a different id/author
|
||||
// (coordinate dedupe above can't catch those). Keeps the freshest copy.
|
||||
spots = deduplicateByIdentity(spots as typeof spots & { tags: string[][]; created_at: number }[]);
|
||||
|
||||
if (params.difficulty && params.difficulty !== 'all') {
|
||||
spots = spots.filter(
|
||||
(e) => getTag(e, 'difficulty')?.toLowerCase() === params.difficulty
|
||||
|
|
|
|||
Loading…
Reference in a new issue