[object Object]

← back to Costa Rica

Localize all region/place images (Wikimedia burst-429 fix) + curated province scenic fallback (locator-SVG/Mikulov cleanup)

4ad9f203f52391d9919b93112469a592b44775eb · 2026-07-22 21:06:56 -0700 · Steve

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Files touched

Diff

commit 4ad9f203f52391d9919b93112469a592b44775eb
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 22 21:06:56 2026 -0700

    Localize all region/place images (Wikimedia burst-429 fix) + curated province scenic fallback (locator-SVG/Mikulov cleanup)
    
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 scripts/cr-fix-fallback.js    | 42 +++++++++++++++++++++++++++
 scripts/cr-localize-images.js | 67 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 109 insertions(+)

diff --git a/scripts/cr-fix-fallback.js b/scripts/cr-fix-fallback.js
new file mode 100644
index 0000000..a5e696c
--- /dev/null
+++ b/scripts/cr-fix-fallback.js
@@ -0,0 +1,42 @@
+'use strict';
+// Replace bad fallback region images (province locator-map SVGs + one wrong
+// Mikulov/Czech photo) with a real scenic Wikipedia lead image per province.
+// Curated pages: tourism-famous, real photos, attributable. Run on Kamatera.
+const { Pool } = require('pg');
+const pool = new Pool({ connectionString: process.env.DATABASE_URL });
+const UA = { 'User-Agent': 'CR-Directory/1.0 (https://costarica.agentabrams.com; info@agentabrams.com)' };
+
+const PROVINCE_SCENIC = {
+  'San José':   'San_José,_Costa_Rica',
+  'Alajuela':   'Arenal_Volcano',
+  'Heredia':    'Barva_Volcano',
+  'Cartago':    'Irazú_Volcano',
+  'Limón':      'Cahuita_National_Park',
+  'Puntarenas': 'Manuel_Antonio_National_Park',
+  'Guanacaste': 'Rincón_de_la_Vieja_Volcano',
+};
+
+(async () => {
+  for (const [prov, page] of Object.entries(PROVINCE_SCENIC)) {
+    const r = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(page)}`, { headers: UA });
+    if (!r.ok) { console.log(`${prov}: summary HTTP ${r.status}`); continue; }
+    const j = await r.json();
+    const img = j.originalimage || j.thumbnail;
+    if (!img || img.source.endsWith('.svg') || img.source.includes('.svg/')) {
+      console.log(`${prov}: no usable photo on ${page}`); continue;
+    }
+    // Only rows still carrying a BAD remote image: locator-map SVG renders or
+    // the known-wrong Mikulov photo. Already-localized and real photos untouched.
+    const res = await pool.query(
+      `UPDATE regions SET image_url=$1,
+              image_credit=$2, image_license='CC via Wikimedia Commons',
+              image_source_url=$3, image_fetched_at=NOW()
+       WHERE province=$4 AND image_url LIKE 'http%'
+         AND (image_url LIKE '%_in_Costa_Rica.svg%' OR image_url LIKE '%Mikulov%')`,
+      [img.source, `Wikipedia: ${j.title}`,
+       j.content_urls?.desktop?.page || `https://en.wikipedia.org/wiki/${page}`, prov]);
+    console.log(`${prov}: ${res.rowCount} regions -> ${page}`);
+    await new Promise(s => setTimeout(s, 500));
+  }
+  await pool.end();
+})();
diff --git a/scripts/cr-localize-images.js b/scripts/cr-localize-images.js
new file mode 100644
index 0000000..11cf079
--- /dev/null
+++ b/scripts/cr-localize-images.js
@@ -0,0 +1,67 @@
+'use strict';
+// costa-rica: localize region + place images. Wikimedia hotlinks 429 on
+// burst-loads (grids fire ~50 at once), so download each once (~900px thumb)
+// into public/img/{regions,places}/ and repoint the DB, preserving the
+// original in image_remote_url. Idempotent: skips rows already local.
+// Run ON Kamatera from /root/Projects/costa-rica with DATABASE_URL exported.
+const fs = require('fs');
+const path = require('path');
+const { Pool } = require('pg');
+
+const pool = new Pool({ connectionString: process.env.DATABASE_URL });
+const UA = 'CR-Directory/1.0 (https://costarica.agentabrams.com; info@agentabrams.com) image-localizer';
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+
+function thumbUrl(remote) {
+  const clean = remote.split('?')[0];
+  if (clean.includes('upload.wikimedia.org')) {
+    // /commons/thumb/<h>/<hh>/<File>/<Npx-File> → real filename is 2nd-to-last
+    const segs = clean.split('/');
+    const file = decodeURIComponent(clean.includes('/thumb/') ? segs[segs.length - 2] : segs[segs.length - 1]);
+    return `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(file)}?width=900`;
+  }
+  return remote; // non-wikimedia: fetch as-is
+}
+
+async function fetchWithRetry(url) {
+  for (let a = 1; a <= 4; a++) {
+    const res = await fetch(url, { headers: { 'User-Agent': UA }, redirect: 'follow' });
+    if (res.status === 429) { console.log(`  429 — backoff 20s (${a})`); await sleep(20000); continue; }
+    return res;
+  }
+  return null;
+}
+
+async function localize(table, dir) {
+  await pool.query(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS image_remote_url TEXT`);
+  const { rows } = await pool.query(
+    `SELECT id, slug, image_url FROM ${table} WHERE image_url LIKE 'http%' ORDER BY slug`);
+  console.log(`${table}: ${rows.length} remote images`);
+  const out = path.join(__dirname, 'public', 'img', dir);
+  fs.mkdirSync(out, { recursive: true });
+  let ok = 0, fail = 0;
+  for (const r of rows) {
+    try {
+      const res = await fetchWithRetry(thumbUrl(r.image_url));
+      if (!res || !res.ok) { console.log(`FAIL ${r.slug}: HTTP ${res && res.status}`); fail++; continue; }
+      const ct = res.headers.get('content-type') || '';
+      const ext = ct.includes('png') ? 'png' : 'jpg';
+      const buf = Buffer.from(await res.arrayBuffer());
+      if (buf.length < 4096) { console.log(`FAIL ${r.slug}: tiny (${buf.length}B)`); fail++; continue; }
+      fs.writeFileSync(path.join(out, `${r.slug}.${ext}`), buf);
+      await pool.query(
+        `UPDATE ${table} SET image_remote_url = image_url, image_url = $1 WHERE id = $2`,
+        [`/img/${dir}/${r.slug}.${ext}`, r.id]);
+      ok++;
+      console.log(`ok ${r.slug} (${Math.round(buf.length / 1024)}KB)`);
+    } catch (e) { console.log(`FAIL ${r.slug}: ${e.message}`); fail++; }
+    await sleep(1100);
+  }
+  console.log(`${table} DONE ok=${ok} fail=${fail}`);
+}
+
+(async () => {
+  await localize('regions', 'regions');
+  await localize('places', 'places');
+  await pool.end();
+})();

← 31e910d guard against serving .bak / .pre- snapshot files  ·  back to Costa Rica  ·  Free website enrichment via OpenStreetMap/Overpass: 89 place 2f8854c →