import { cleanText } from '../lead-engine.mjs'; import { sleep } from '../util/collections.mjs'; import { UA_HEADERS, UF_NAMES, buildLeadFromPlace } from './common.mjs'; import { fetchTextWithTimeout } from '../util/fetch.mjs'; async function nominatimSearch(baseUrl, query, limit) { const url = new URL(baseUrl); url.searchParams.set('format', 'jsonv2'); url.searchParams.set('q', query); url.searchParams.set('countrycodes', 'br'); url.searchParams.set('addressdetails', '1'); url.searchParams.set('limit', String(Math.max(1, limit))); const { response, text } = await fetchTextWithTimeout(url, { headers: UA_HEADERS }); if (!response.ok) { throw new Error(`Nominatim HTTP ${response.status}: ${text.slice(0, 300)}`); } return JSON.parse(text); } function isResultInTarget(result, city, uf) { const address = result.address || {}; const resultCity = address.city || address.town || address.village || address.municipality || ''; const resultState = cleanText(address.state || ''); const expectedState = cleanText(UF_NAMES[uf] || uf); const currentCity = cleanText(resultCity); const expectedCity = cleanText(city); return resultState === expectedState && (currentCity === expectedCity || currentCity.includes(expectedCity) || expectedCity.includes(currentCity)); } function resultToPlace(result, segment, city, uf, query) { const name = result.name || result.display_name?.split(',')[0]; if (!name) return null; const address = result.address || {}; return { name, phone: '', city: address.city || address.town || address.village || city, state: uf, segment: segment.name, website: '', address: result.display_name || '', formatted_address: result.display_name || '', query, osm_id: String(result.osm_id || ''), osm_type: result.osm_type || '' }; } // Fonte nominatim (OpenStreetMap): busca textual termo a termo por cidade, // validando que o resultado pertence mesmo a cidade/UF pedida. export async function* collect(params, ctx) { const { uf, cities, segments, limits } = params; if (!cities || !cities.length) { ctx.log('nominatim: nenhuma cidade selecionada — fonte pulada'); return; } let executedQueries = 0; for (const city of cities) { const label = `${city} ${uf}`; const cityLeads = []; for (const segment of segments) { for (const queryTerm of segment.queries || []) { if (ctx.isCancelled()) return; if (limits.queryLimit > 0 && executedQueries >= limits.queryLimit) { if (cityLeads.length) yield { origin: 'nominatim', label, leads: cityLeads }; return; } const query = `${queryTerm} ${city} ${uf}`; executedQueries += 1; ctx.emit({ phase: 'nominatim', message: query }); let results = []; try { results = await nominatimSearch(ctx.endpoints.nominatimUrl, query, limits.maxPerQuery || 10); } catch (error) { ctx.log(`nominatim falhou em ${query}: ${error.message}`); await sleep(Math.max(ctx.delays.overpassDelayMs, 5000)); continue; } for (const result of results) { if (!isResultInTarget(result, city, uf)) continue; const place = resultToPlace(result, segment, city, uf, query); if (!place) continue; cityLeads.push(buildLeadFromPlace(place, segment, city, uf, 'nominatim')); } await sleep(Math.max(ctx.delays.overpassDelayMs, 1200)); } } if (cityLeads.length) yield { origin: 'nominatim', label, leads: cityLeads }; } }