Created
October 10, 2025 00:53
-
-
Save scottfwalter/07d4d7d7245182512c7f684fa2260b0a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // create-national-park-md.js | |
| // Usage: node create-national-park-md.js [outputDir] | |
| // Example: node create-national-park-md.js ./parks | |
| // If no outputDir is provided, it writes to ./national-parks | |
| import fs from 'node:fs/promises' | |
| import path from 'node:path' | |
| // --- Configuration --- | |
| const OUTPUT_DIR = process.argv[2] || path.resolve('./national-parks') | |
| const RATE_LIMIT_MS = 1100 // be polite to Nominatim | |
| const NOMINATIM_BASE = 'https://nominatim.openstreetmap.org/search' | |
| // Prefer US + territories where NPS parks live | |
| const COUNTRY_CODES = 'us,vi,as,gu,mp,pr' | |
| // Parks list (63). Keep the exact display names for filenames. | |
| const PARKS = [ | |
| 'Acadia National Park', | |
| 'American Samoa National Park', | |
| 'Arches National Park', | |
| 'Badlands National Park', | |
| 'Big Bend National Park', | |
| 'Biscayne National Park', | |
| 'Black Canyon of the Gunnison National Park', | |
| 'Bryce Canyon National Park', | |
| 'Canyonlands National Park', | |
| 'Capitol Reef National Park', | |
| 'Carlsbad Caverns National Park', | |
| 'Channel Islands National Park', | |
| 'Congaree National Park', | |
| 'Crater Lake National Park', | |
| 'Cuyahoga Valley National Park', | |
| 'Death Valley National Park', | |
| 'Denali National Park', | |
| 'Dry Tortugas National Park', | |
| 'Everglades National Park', | |
| 'Gates of the Arctic National Park', | |
| 'Gateway Arch National Park', | |
| 'Glacier National Park', | |
| 'Glacier Bay National Park', | |
| 'Grand Canyon National Park', | |
| 'Grand Teton National Park', | |
| 'Great Basin National Park', | |
| 'Great Sand Dunes National Park', | |
| 'Great Smoky Mountains National Park', | |
| 'Guadalupe Mountains National Park', | |
| 'Haleakalā National Park', | |
| 'Hawaiʻi Volcanoes National Park', | |
| 'Hot Springs National Park', | |
| 'Indiana Dunes National Park', | |
| 'Isle Royale National Park', | |
| 'Joshua Tree National Park', | |
| 'Katmai National Park', | |
| 'Kenai Fjords National Park', | |
| 'Kings Canyon National Park', | |
| 'Kobuk Valley National Park', | |
| 'Lake Clark National Park', | |
| 'Lassen Volcanic National Park', | |
| 'Mammoth Cave National Park', | |
| 'Mesa Verde National Park', | |
| 'Mount Rainier National Park', | |
| 'New River Gorge National Park', | |
| 'North Cascades National Park', | |
| 'Olympic National Park', | |
| 'Petrified Forest National Park', | |
| 'Pinnacles National Park', | |
| 'Redwood National Park', | |
| 'Rocky Mountain National Park', | |
| 'Saguaro National Park', | |
| 'Sequoia National Park', | |
| 'Shenandoah National Park', | |
| 'Theodore Roosevelt National Park', | |
| 'Virgin Islands National Park', | |
| 'Voyageurs National Park', | |
| 'White Sands National Park', | |
| 'Wind Cave National Park', | |
| 'Wrangell–St. Elias National Park', | |
| 'Yellowstone National Park', | |
| 'Yosemite National Park', | |
| 'Zion National Park', | |
| ] | |
| // Optional manual coordinate overrides for tricky names (lon, lat as strings). | |
| // If you want to pin a specific location (e.g., park HQ or a central point), add it here. | |
| // By default the script will geocode; these entries will skip the API for that park. | |
| const MANUAL_OVERRIDES = { | |
| // Example: | |
| // "Grand Teton National Park": ["-110.6818", "43.7904"], | |
| } | |
| const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) | |
| function toYAMLFrontmatter({ lon, lat }) { | |
| // Ensure longitude first, then latitude, both as quoted strings. | |
| return `--- | |
| tags: | |
| - nationalpark | |
| coordinates: | |
| - "${lat}" | |
| - "${lon}" | |
| color: green | |
| icon: tree-pine | |
| ---` | |
| } | |
| function sanitizeFilename(name) { | |
| // The user wants the filename to be the name of the park. | |
| // Most filesystems support these characters; keep as-is except forbid slashes and colons. | |
| const cleaned = name.replaceAll('/', '⁄').replaceAll(':', 'ː') | |
| return `${cleaned}.md` | |
| } | |
| async function geocodePark(name) { | |
| // Prefer searching for the exact park name as-is; include “National Park” already present. | |
| const params = new URLSearchParams({ | |
| q: name, | |
| format: 'jsonv2', | |
| addressdetails: '0', | |
| dedupe: '1', | |
| limit: '1', | |
| countrycodes: COUNTRY_CODES, | |
| }) | |
| const url = `${NOMINATIM_BASE}?${params.toString()}` | |
| const res = await fetch(url, { | |
| headers: { | |
| 'User-Agent': 'national-parks-markdown/1.0 (contact: example@example.com)', | |
| }, | |
| }) | |
| if (!res.ok) { | |
| throw new Error(`Geocoding failed for ${name}: ${res.status} ${res.statusText}`) | |
| } | |
| const data = await res.json() | |
| if (!Array.isArray(data) || data.length === 0) { | |
| throw new Error(`No geocoding result for ${name}`) | |
| } | |
| const { lon, lat } = data[0] // strings | |
| if (!lon || !lat) { | |
| throw new Error(`Missing lon/lat for ${name}`) | |
| } | |
| return { lon: String(lon), lat: String(lat) } | |
| } | |
| async function main() { | |
| await fs.mkdir(OUTPUT_DIR, { recursive: true }) | |
| for (let i = 0; i < PARKS.length; i++) { | |
| const name = PARKS[i] | |
| const filename = sanitizeFilename(name) | |
| const outPath = path.join(OUTPUT_DIR, filename) | |
| try { | |
| let coords | |
| if (MANUAL_OVERRIDES[name]) { | |
| const [lon, lat] = MANUAL_OVERRIDES[name] | |
| coords = { lon, lat } | |
| } else { | |
| coords = await geocodePark(name) | |
| await sleep(RATE_LIMIT_MS) // be polite between requests | |
| } | |
| const frontmatter = toYAMLFrontmatter(coords) | |
| const content = `${frontmatter}\n\n# ${name}\n` | |
| await fs.writeFile(outPath, content, 'utf8') | |
| console.log(`✓ Wrote ${filename}`) | |
| } catch (err) { | |
| console.error(`✗ Failed ${name}: ${(err && err.message) || err}`) | |
| } | |
| } | |
| console.log('\nDone.') | |
| } | |
| main().catch((e) => { | |
| console.error(e) | |
| process.exit(1) | |
| }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment