teqo/src/lib/wizardMunicipalitySuggestMerge.ts
Cursor Agent 7e6bc9d984
B125 — nearest município first in search suggest + wizard
- Restore B117 home search: scopeMunicipalities, merge prefix, session prompt
- Shared useNearestMunicipalitySlug (B14 key) for busca geral + wizard
- Wizard merge uses geoSlug only — no Perto de você / km copy on rows
- Impl plan: docs/plans/municipio-geo-topo-sugestoes-busca-impl.md

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 18:21:52 +00:00

120 lines
3.5 KiB
TypeScript

import type { HomeSearchMunicipalityHit } from '@/lib/campaignHomeSearchHits'
import { HOME_SEARCH_SUGGEST_LIMIT } from '@/lib/homeSearchSuggest'
export const WIZARD_CONTINUITY_LAST_ACTED_LABEL = 'Última ação' as const
export const WIZARD_CONTINUITY_VISITED_LABEL = 'Visitado' as const
const WIZARD_CONTINUITY_MAX_VISITED = 3
type WizardContinuitySource = 'last-acted' | 'visited'
export type WizardContinuitySlug = {
source: WizardContinuitySource
slug: string
}
export type WizardContinuityVisitInput = {
href: string
kind: 'municipality' | 'municipalityList'
}
export type WizardMunicipalityMergedRow = {
hit: HomeSearchMunicipalityHit
continuityReason?: string
}
const MUNICIPALITY_DETAIL_HREF_PREFIX = '/campanha/municipios/'
export const municipalitySlugFromRecentVisitHref = (href: string): string | null => {
if (!href.startsWith(MUNICIPALITY_DETAIL_HREF_PREFIX)) return null
const slug = href.slice(MUNICIPALITY_DETAIL_HREF_PREFIX.length).split('/')[0]?.trim()
return slug ? slug : null
}
const continuityReasonForSource = (source: WizardContinuitySource): string => {
switch (source) {
case 'last-acted':
return WIZARD_CONTINUITY_LAST_ACTED_LABEL
case 'visited':
return WIZARD_CONTINUITY_VISITED_LABEL
default: {
const _exhaustive: never = source
return _exhaustive
}
}
}
export const listWizardContinuitySlugs = (input: {
lastActedSlug: string | null
recentVisits: WizardContinuityVisitInput[]
scopeSlugs: ReadonlySet<string>
maxVisited?: number
}): WizardContinuitySlug[] => {
const maxVisited = input.maxVisited ?? WIZARD_CONTINUITY_MAX_VISITED
const continuity: WizardContinuitySlug[] = []
const seen = new Set<string>()
const pushSlug = (source: WizardContinuitySource, slug: string) => {
if (!input.scopeSlugs.has(slug) || seen.has(slug)) return
seen.add(slug)
continuity.push({ source, slug })
}
if (input.lastActedSlug) {
pushSlug('last-acted', input.lastActedSlug)
}
let visitedCount = 0
for (const entry of input.recentVisits) {
if (visitedCount >= maxVisited) break
if (entry.kind !== 'municipality') continue
const slug = municipalitySlugFromRecentVisitHref(entry.href)
if (!slug) continue
if (seen.has(slug)) continue
if (!input.scopeSlugs.has(slug)) continue
seen.add(slug)
continuity.push({ source: 'visited', slug })
visitedCount += 1
}
return continuity
}
/**
* B93 + B125 — merge geo prefix, continuity (B93) and server suggestions (B92).
* Dedup order: geo → last-acted → visited → forgotten. Geo row has no proximity copy.
*/
export const mergeWizardMunicipalitySuggestions = (input: {
continuity: WizardContinuitySlug[]
geoSlug?: string | null
serverHits: HomeSearchMunicipalityHit[]
hitBySlug: ReadonlyMap<string, HomeSearchMunicipalityHit>
limit?: number
}): WizardMunicipalityMergedRow[] => {
const limit = input.limit ?? HOME_SEARCH_SUGGEST_LIMIT
const merged: WizardMunicipalityMergedRow[] = []
const seen = new Set<string>()
const pushHit = (slug: string, continuityReason?: string) => {
if (seen.has(slug)) return
const hit = input.hitBySlug.get(slug)
if (!hit) return
seen.add(slug)
merged.push({ hit, continuityReason })
}
const geoSlug = input.geoSlug?.trim()
if (geoSlug) {
pushHit(geoSlug)
}
for (const item of input.continuity) {
pushHit(item.slug, continuityReasonForSource(item.source))
}
for (const hit of input.serverHits) {
pushHit(hit.slug)
}
return merged.slice(0, limit)
}