[object Object]

← 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

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)