← back to Costa Rica
scripts/ingest/ict-cst.js
118 lines
'use strict';
// ICT CST — Certificación para la Sostenibilidad Turística
// Public directory of CR-government-certified sustainable-tourism businesses.
// Source: https://turismo-sostenible.co.cr/entidades-certificadas/
const { fetchText, slug, sleep, regionMap, resolveRegion, upsertPlace, startRun, finishRun, pool } = require('./_lib');
const ROOT = 'https://turismo-sostenible.co.cr/entidades-certificadas/';
const VERTICAL_BY_CATEGORY = {
'hospedaje': 'tourism_hotel',
'alojamiento': 'tourism_hotel',
'tour operadores': 'tourism_tour',
'tour-operadores': 'tourism_tour',
'tour operador': 'tourism_tour',
'agencia de viajes': 'tourism_tour',
'gastronomia': 'tourism_restaurant',
'gastronomía': 'tourism_restaurant',
'restaurante': 'tourism_restaurant',
'transporte': 'tourism_tour',
'rent a car': 'tourism_tour',
'spa': 'service_beauty',
'wellness': 'service_beauty',
};
function pickVertical(category) {
const c = (category||'').toLowerCase().trim();
for (const [k,v] of Object.entries(VERTICAL_BY_CATEGORY)) {
if (c.includes(k)) return v;
}
return 'tourism_hotel';
}
function extractListings(html) {
// Conservative regex parse; the page renders entity cards as anchor tags.
// We avoid heavy DOM parsing to keep the script dep-light.
const out = [];
const cardRe = /<article[^>]*class="[^"]*entidad[^"]*"[^>]*>([\s\S]*?)<\/article>/gi;
let m;
while ((m = cardRe.exec(html)) !== null) {
const block = m[1];
const name = (block.match(/<h2[^>]*>\s*([^<]+?)\s*<\/h2>/i)||[])[1] || (block.match(/<h3[^>]*>\s*([^<]+?)\s*<\/h3>/i)||[])[1];
const province = (block.match(/Provincia[^:]*:\s*<\/?[a-z]+>?\s*([^<]+?)\s*</i)||[])[1];
const canton = (block.match(/Cant[oó]n[^:]*:\s*<\/?[a-z]+>?\s*([^<]+?)\s*</i)||[])[1];
const distrito = (block.match(/Distrito[^:]*:\s*<\/?[a-z]+>?\s*([^<]+?)\s*</i)||[])[1];
const categoria = (block.match(/Categor[ií]a[^:]*:\s*<\/?[a-z]+>?\s*([^<]+?)\s*</i)||[])[1];
const link = (block.match(/<a[^>]+href="([^"]+)"/i)||[])[1];
const img = (block.match(/<img[^>]+src="([^"]+)"/i)||[])[1];
if (name) {
out.push({
name: name.trim(),
category: 'tourism',
vertical: pickVertical(categoria),
province: (province||'').trim(),
canton: (canton||'').trim(),
distrito: (distrito||'').trim(),
source: 'ict-cst',
source_url: link ? new URL(link, ROOT).toString() : ROOT,
image_url: img ? new URL(img, ROOT).toString() : null,
address: [distrito, canton, province].filter(Boolean).join(', '),
});
}
}
return out;
}
(async () => {
const runId = await startRun('ict-cst', 'Scrape ICT CST sustainable-tourism directory');
const rmap = await regionMap();
let html, listings = [], inserted = 0, updated = 0;
try {
html = await fetchText(ROOT);
listings = extractListings(html);
if (!listings.length) {
// Fallback: many WP sites use AJAX; if direct HTML had no <article class=entidad>, log markup hint
const hint = html.match(/class="[^"]*entidad[^"]*"/i)?.[0] || '(no entidad class found)';
console.warn(`[ict-cst] no listings parsed from root HTML; hint=${hint}`);
}
for (const l of listings) {
const region = resolveRegion(rmap, l.canton || l.distrito || l.province);
const placeSlug = slug(`ict-${l.name}-${region.slug}`);
const r = await upsertPlace({
slug: placeSlug,
name: l.name,
category: 'tourism',
vertical: l.vertical,
region_id: region.id,
description: `ICT CST-certified ${l.vertical.replace('tourism_','')} in ${l.canton||l.province||'Costa Rica'}.`,
address: l.address,
image_url: l.image_url,
source: l.source,
source_url: l.source_url,
});
if (r.inserted) inserted++; else updated++;
await sleep(50);
}
await finishRun(runId, {
rows_in: listings.length,
rows_added: inserted,
rows_updated: updated,
status: listings.length ? 'ok' : 'empty',
notes: listings.length ? '' : 'parser found no <article class=entidad> blocks; ICT page likely AJAX-rendered, switch to Browserbase'
});
console.log(`[ict-cst] in=${listings.length} added=${inserted} updated=${updated}`);
} catch (e) {
await finishRun(runId, { status: 'error', notes: e.message });
console.error('[ict-cst] FAIL', e.message);
process.exitCode = 1;
} finally {
await pool.end();
}
})();