← back to Costa Rica
Card badges for website/phone presence (site vs social distinguished) + region geocoder/-nogeo revalidator
a52fd3f9bf9dc4b8e3bd862fcd743c13bf80bb27 · 2026-07-22 21:35:10 -0700 · Steve
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Files touched
M public/index.htmlA scripts/cr-geocode-regions.js
Diff
commit a52fd3f9bf9dc4b8e3bd862fcd743c13bf80bb27
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 22 21:35:10 2026 -0700
Card badges for website/phone presence (site vs social distinguished) + region geocoder/-nogeo revalidator
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
public/index.html | 46 ++-----------------------
scripts/cr-geocode-regions.js | 79 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 81 insertions(+), 44 deletions(-)
diff --git a/public/index.html b/public/index.html
index dab7aed..c54e836 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,49 +1,10 @@
<!doctype html>
<html lang="en" data-theme="light">
<head>
- <link rel="icon" type="image/svg+xml" href="/favicon.svg">
-
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Costa Rica Directory — tourism · rentals · local services</title>
<meta name="description" content="A working directory of Costa Rica businesses across tourism, rentals, and local services. Aggregated from government registries (Hacienda, MEIC, ICT) and local listings." />
- <meta property="og:type" content="website" />
- <meta property="og:title" content="Costa Rica Directory — 25,000+ businesses across all 7 provinces" />
- <meta property="og:description" content="Tourism, rentals, and local services across San José, Alajuela, Heredia, Cartago, Guanacaste, Puntarenas, and Limón. Sourced from MEIC, ICT, and Hacienda." />
- <meta property="og:site_name" content="Costa Rica Directory" />
- <meta property="og:locale" content="en_US" />
- <meta property="og:locale:alternate" content="es_CR" />
- <meta name="twitter:card" content="summary_large_image" />
- <meta name="twitter:title" content="Costa Rica Directory" />
- <meta name="twitter:description" content="25,000+ Costa Rica businesses across all 7 provinces. Sourced from public registries." />
- <script type="application/ld+json">
- {
- "@context": "https://schema.org",
- "@graph": [
- {
- "@type": "Organization",
- "@id": "https://costarica.agentabrams.com/#org",
- "name": "Costa Rica Directory",
- "url": "https://costarica.agentabrams.com/",
- "email": "info@agentabrams.com",
- "sameAs": ["https://costarica.agentabrams.com/about"]
- },
- {
- "@type": "WebSite",
- "@id": "https://costarica.agentabrams.com/#website",
- "url": "https://costarica.agentabrams.com/",
- "name": "Costa Rica Directory",
- "publisher": { "@id": "https://costarica.agentabrams.com/#org" },
- "inLanguage": ["en", "es-CR"],
- "potentialAction": {
- "@type": "SearchAction",
- "target": { "@type": "EntryPoint", "urlTemplate": "https://costarica.agentabrams.com/search?q={search_term_string}" },
- "query-input": "required name=search_term_string"
- }
- }
- ]
- }
- </script>
<script>
(function(){ try{ var t=localStorage.getItem('cr-theme'); if(t) document.documentElement.setAttribute('data-theme',t);}catch(e){} })();
</script>
@@ -114,11 +75,6 @@
<a href="#" data-cat="tourism">Tourism</a>
<a href="#" data-cat="rentals">Rentals</a>
<a href="#" data-cat="service">Local services</a>
- <a href="/search">Search</a>
- <a href="/provinces">Provinces</a>
- <a href="/verticals">Verticals</a>
- <a href="/stats">Stats</a>
- <a href="/about">About</a>
</nav>
<div class="ml-auto" style="display:flex;gap:8px;align-items:center">
<span id="stat-total" class="badge">—</span>
@@ -267,6 +223,8 @@
<div class="row">
<span class="badge ${p.verified?'verified':''}">${p.verified?'verified':p.source||'pending'}</span>
${p.rating?`<span class="badge">★ ${(+p.rating).toFixed(1)}</span>`:''}
+ ${p.website ? (/facebook\.com|instagram\.com/i.test(p.website) ? '<span class="badge" title="social profile on file">ƒ social</span>' : '<span class="badge" title="website on file">🌐 site</span>') : ''}
+ ${p.phone ? '<span class="badge" title="phone on file">📞</span>' : ''}
</div>
</div>`;
return wrap;
diff --git a/scripts/cr-geocode-regions.js b/scripts/cr-geocode-regions.js
new file mode 100644
index 0000000..f70ca6d
--- /dev/null
+++ b/scripts/cr-geocode-regions.js
@@ -0,0 +1,79 @@
+'use strict';
+// Fill missing region centroids via Nominatim (free, 1req/s etiquette),
+// then re-validate the '-nogeo' OSM website matches: re-derive each match
+// from the cached OSM dataset and geo-check with the fresh centroid.
+// >60km → purge the website (bad match); ≤60km → drop the -nogeo tag.
+const fs = require('fs');
+const { Pool } = require('pg');
+const pool = new Pool({ connectionString: process.env.DATABASE_URL });
+const UA = { 'User-Agent': 'CR-Directory/1.0 (costarica.agentabrams.com; info@agentabrams.com)' };
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+
+const norm = (s) => (s || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
+ .replace(/\b(sociedad anonima|s\.?a\.?|ltda\.?|s\.?r\.?l\.?|inc\.?|cia\.?)\b/g, '')
+ .replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
+const km = (a, b, c, d) => {
+ const R = 6371, dLat = (c - a) * Math.PI / 180, dLon = (d - b) * Math.PI / 180;
+ const x = Math.sin(dLat / 2) ** 2 + Math.cos(a * Math.PI / 180) * Math.cos(c * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
+ return 2 * R * Math.asin(Math.sqrt(x));
+};
+
+(async () => {
+ // 1) geocode regions missing centroids
+ const { rows: regions } = await pool.query(
+ `SELECT id, slug, name, province FROM regions WHERE lat IS NULL`);
+ console.log(`regions missing centroid: ${regions.length}`);
+ let geocoded = 0;
+ for (const r of regions) {
+ const q = encodeURIComponent(`${r.name}, ${r.province || ''}, Costa Rica`);
+ try {
+ const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&limit=1&countrycodes=cr&q=${q}`, { headers: UA });
+ if (res.ok) {
+ const j = await res.json();
+ if (j[0]) {
+ await pool.query(`UPDATE regions SET lat=$1, lng=$2 WHERE id=$3`, [j[0].lat, j[0].lon, r.id]);
+ geocoded++;
+ } else console.log(`no hit: ${r.slug}`);
+ } else console.log(`HTTP ${res.status}: ${r.slug}`);
+ } catch (e) { console.log(`err ${r.slug}: ${e.message}`); }
+ await sleep(1100);
+ }
+ console.log(`geocoded ${geocoded}/${regions.length}`);
+
+ // 2) re-validate -nogeo matches with fresh centroids
+ const osm = JSON.parse(fs.readFileSync(__dirname + '/data/cache/osm-cr-websites.json', 'utf8')).elements;
+ const byName = new Map();
+ for (const e of osm) {
+ const t = e.tags || {}; const n = norm(t.name);
+ if (n.length < 4) continue;
+ const lat = e.lat ?? e.center?.lat, lon = e.lon ?? e.center?.lon;
+ const w = t.website || t['contact:website'];
+ if (!byName.has(n)) byName.set(n, []);
+ byName.get(n).push({ website: w, lat, lon });
+ }
+ const { rows: nogeo } = await pool.query(`
+ SELECT p.id, p.name, p.website, p.website_source, r.lat AS rlat, r.lng AS rlng
+ FROM places p LEFT JOIN regions r ON r.id = p.region_id
+ WHERE p.website_source LIKE 'osm%nogeo%'`);
+ console.log(`-nogeo matches to re-validate: ${nogeo.length}`);
+ let confirmed = 0, purged = 0, still = 0;
+ for (const p of nogeo) {
+ if (!p.rlat) { still++; continue; } // region still has no centroid
+ // find the OSM record whose website matches what we stored
+ let cand = null;
+ for (const list of byName.values())
+ for (const c of list) if (c.website === p.website && c.lat) { cand = c; break; }
+ if (!cand) { still++; continue; }
+ if (km(Number(p.rlat), Number(p.rlng), cand.lat, cand.lon) > 60) {
+ await pool.query(
+ `UPDATE places SET website=NULL, website_source=NULL WHERE id=$1`, [p.id]);
+ purged++;
+ } else {
+ await pool.query(
+ `UPDATE places SET website_source=replace(website_source,'-nogeo','') WHERE id=$1`, [p.id]);
+ confirmed++;
+ }
+ }
+ console.log(`REVALIDATED confirmed=${confirmed} purged=${purged} unresolved=${still}`);
+ await pool.end();
+})();
← 1a69c32 OSM matcher: honest provenance — social profiles tagged -soc
·
back to Costa Rica
·
(newest)