Baseline commit of the existing Maquina Comercial Pessoal project

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.
This commit is contained in:
Gustavo
2026-07-31 10:54:07 -03:00
parent 6cd336a41e
commit d330bb5ff0
132 changed files with 29969 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
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 };
}
}