code icon Code

Geocode Location

Convert address to coordinates (forward) or coordinates to address (reverse) using Nominatim

Source Code

const [query, mode = "forward"] = process.argv.slice(2);

if (!query) {
  console.error("Error: query is required");
  process.exit(1);
}

const USER_AGENT = "SaunaGeoOperations/1.0";

// Check if string looks like coordinates (lat,lon)
function isCoordinates(str) {
  const coordPattern = /^-?\d+\.?\d*,\s*-?\d+\.?\d*$/;
  return coordPattern.test(str.trim());
}

// Forward geocode: address -> coordinates
async function forwardGeocode(address) {
  const params = new URLSearchParams({
    q: address,
    format: "json",
    limit: "1",
    addressdetails: "1",
  });

  const response = await fetch(
    `https://nominatim.openstreetmap.org/search?${params}`,
    { headers: { "User-Agent": USER_AGENT } }
  );

  if (!response.ok) {
    throw new Error(`Geocoding failed: ${response.status}`);
  }

  const results = await response.json();
  if (!results.length) {
    throw new Error(`Location not found: "${address}"`);
  }

  const result = results[0];
  return {
    lat: parseFloat(result.lat),
    lon: parseFloat(result.lon),
    displayName: result.display_name,
    address: result.address || {},
    boundingBox: result.boundingbox
      ? {
          south: parseFloat(result.boundingbox[0]),
          north: parseFloat(result.boundingbox[1]),
          west: parseFloat(result.boundingbox[2]),
          east: parseFloat(result.boundingbox[3]),
        }
      : null,
  };
}

// Reverse geocode: coordinates -> address
async function reverseGeocode(lat, lon) {
  const params = new URLSearchParams({
    lat: lat.toString(),
    lon: lon.toString(),
    format: "json",
    addressdetails: "1",
  });

  const response = await fetch(
    `https://nominatim.openstreetmap.org/reverse?${params}`,
    { headers: { "User-Agent": USER_AGENT } }
  );

  if (!response.ok) {
    throw new Error(`Reverse geocoding failed: ${response.status}`);
  }

  const result = await response.json();
  if (result.error) {
    throw new Error(`Reverse geocoding failed: ${result.error}`);
  }

  return {
    lat: parseFloat(result.lat),
    lon: parseFloat(result.lon),
    displayName: result.display_name,
    address: result.address || {},
  };
}

async function main() {
  if (mode === "reverse") {
    // Reverse geocoding: expect coordinates
    if (!isCoordinates(query)) {
      console.error(
        "Error: reverse mode requires coordinates in 'lat,lon' format"
      );
      process.exit(1);
    }

    const [latStr, lonStr] = query.split(",").map((s) => s.trim());
    const lat = parseFloat(latStr);
    const lon = parseFloat(lonStr);

    console.log(`Reverse geocoding: ${lat}, ${lon}`);
    const result = await reverseGeocode(lat, lon);

    console.log(`Found: ${result.displayName}`);
    console.log(
      JSON.stringify({
        success: true,
        mode: "reverse",
        input: { lat, lon },
        result,
      })
    );
  } else {
    // Forward geocoding: address -> coordinates
    // If already coordinates, still run through Nominatim for validation/enrichment
    console.log(`Forward geocoding: "${query}"`);
    const result = await forwardGeocode(query);

    console.log(`Found: ${result.displayName}`);
    console.log(`Coordinates: ${result.lat}, ${result.lon}`);
    console.log(
      JSON.stringify({
        success: true,
        mode: "forward",
        input: query,
        result,
      })
    );
  }
}

main().catch((err) => {
  console.error("Geocoding failed:", err.message);
  process.exit(1);
});