import type { NewsItem } from '../types'; import { decodeHtmlEntities } from '../utils/html-entities'; export function normalizeHeadlineKey(title: string): string { return decodeHtmlEntities(title) .toLowerCase() .normalize('NFKD') .replace(/[\u0300-\u036f]/g, '') .replace(/[^a-z0-9]+/g, ' ') .trim() .split(' ') .filter((w) => w.length > 2) .slice(0, 8) .join(' '); } function fallbackHeadlineKey(title: string): string { return decodeHtmlEntities(title).toLowerCase().trim().replace(/\s+/g, ' '); } export interface DedupedHeadline { item: NewsItem; extraSources: string[]; } export type TierLookup = (item: NewsItem) => number; export function dedupeHeadlines( items: NewsItem[], tierOf?: TierLookup, ): DedupedHeadline[] { const getTier: TierLookup = tierOf ?? ((it) => (typeof it.tier === 'number' ? it.tier : 4)); const groups = new Map(); const order: string[] = []; for (const it of items) { let key = normalizeHeadlineKey(it.title); if (!key) key = fallbackHeadlineKey(it.title); if (!key) key = it.link || `__idx_${order.length}`; const existing = groups.get(key); if (existing) { existing.push(it); } else { groups.set(key, [it]); order.push(key); } } const out: DedupedHeadline[] = []; for (const key of order) { const group = groups.get(key); if (!group || group.length === 0) continue; const primary = [...group].sort((a, b) => { const ta = getTier(a); const tb = getTier(b); if (ta !== tb) return ta - tb; const da = a.pubDate instanceof Date ? a.pubDate.getTime() : new Date(a.pubDate).getTime(); const db = b.pubDate instanceof Date ? b.pubDate.getTime() : new Date(b.pubDate).getTime(); return (Number.isFinite(db) ? db : 0) - (Number.isFinite(da) ? da : 0); })[0]!; const extraSources: string[] = []; for (const other of group) { if (other === primary) continue; if (other.source && other.source !== primary.source && !extraSources.includes(other.source)) { extraSources.push(other.source); } } out.push({ item: primary, extraSources }); } return out; }