Dos buscadores en el menú lateral, mismo motor de IA y mismo streaming en directo:
En ambos casos las tarjetas aparecen una a una según se encuentran, y al terminar puedes Ampliar búsqueda (busca más, excluyendo las ya mostradas) o Descargar JSON.
Hay dos WebSockets con el mismo protocolo de eventos en streaming. El cliente abre la
conexión, envía un único mensaje JSON con la petición y a partir de ahí solo recibe
eventos { "type": "...", "data": { ... } }. Tras complete (o un
error) el servidor cierra la conexión. Ambos exigen API key
(?api_key=… en la URL o campo "api_key" en el primer mensaje; la web la
inyecta automáticamente).
1 · /company-search — empresas. Petición:
{
"formacion": { "url": "https://…", "descripcion": "texto…" }, // al menos uno
"ubicacion": { "texto": "provincia de Jaén, España" }, // o lat/lng/radio_km
"max_empresas": 30, // opcional (1–60)
"excluir": ["Empresa A S.L."] // opcional
}
Eventos: status, discovery
({ total, empresas:[nombres] }), company (una ficha por
empresa), discarded, complete
({ total, empresas:[fichas], descartadas, coste }), error.
2 · /center-search — centros de FP privados. Petición (solo región + nº):
{
"region": "provincia de Jaén, España",
"max_centros": 10, // opcional (1–60)
"excluir": ["Centro X"] // opcional
}
Eventos: status, discovery
({ total, centros:[nombres] }), centro (una ficha por centro,
ver abajo), discarded, complete
({ total, centros:[fichas], descartadas, coste }), error.
Ficha de centro (evento centro):
{
"nombre": "…",
"cif": "…", // string | null
"web": "https://…", // string | null
"linkedin": "https://…", // string | null
"telefonos": ["…"], "emails": ["…"],// contacto general del centro
"sedes": [ { "direccion": "…", "ciudad": "…", "provincia": "…", "pais": "…",
"codigo_postal": "…", "coordenadas": { "lat": 0, "lng": 0 },
"en_zona": true } ],
"titulaciones": [
{
"nombre": "CFGS Desarrollo de Aplicaciones Web",
"ubicaciones": ["Sede Jaén"], // dónde se imparte
"web": "https://…/daw", // página específica de la titulación | null
"contacto_nombre": "…", // contacto ESPECÍFICO de la titulación
"contacto_email": "…", // (null si no hay uno distinto del general)
"contacto_telefono": "…"
}
],
"descripcion": "…",
"fuentes": [ { "url": "https://…", "titulo": "…" } ],
"descartada": false, "motivo_descarte": null
}
Las coordenadas de cada sede se geocodifican en el servidor (Mapbox);
los campos no localizables de forma fiable llegan como null o lista vacía. La búsqueda
usa gpt-5.4-mini (descubrimiento) + deepseek-v4-flash (enriquecimiento).