Tracks the app that was already in place (capture, treatment, outreach, WhatsApp integration, server, frontend, tests, docs) so future feature work can branch and use isolated worktrees.
104 lines
3.5 KiB
JavaScript
104 lines
3.5 KiB
JavaScript
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 };
|
|
}
|
|
}
|