Skip to content

Replace Netlify function with public APIs for location search#78

Draft
Copilot wants to merge 3 commits into
mainfrom
copilot/update-location-search-function
Draft

Replace Netlify function with public APIs for location search#78
Copilot wants to merge 3 commits into
mainfrom
copilot/update-location-search-function

Conversation

Copy link
Copy Markdown

Copilot AI commented Nov 21, 2025

The location search currently depends on a Netlify function at strong-mermaid-23f3ab.netlify.app which isn't available for all deployments. This replaces it with public APIs: countriesnow.space for city/country data and Nominatim for geocoding.

Changes

Data fetching:

  • getCoordinates() now fetches from https://countriesnow.space/api/v0.1/countries
  • Returns { country, city, display } objects instead of coordinates
  • Case-insensitive substring matching on country and city names
  • Deduplicates results by display label

Geocoding:

  • New geocodeLocation(city, country) helper uses Nominatim API
  • Returns { lat, lon } or null
  • Includes User-Agent header per Nominatim usage policy

Flow:

  • handleSearch(): Single match → geocode → navigate; multiple matches → show choices
  • renderChoices(): Geocodes on selection with "Resolving location..." feedback
// Old flow: Netlify function returns coordinates directly
const coordinates = await fetch(netlifyUrl).then(r => r.json());
loadWeatherPage(coordinates.lat, coordinates.lon);

// New flow: Fetch cities, then geocode selected location
const matches = await getCoordinates(); // Returns city/country data
const coords = await geocodeLocation(match.city, match.country);
loadWeatherPage(coords.lat, coords.lon);

API considerations

Nominatim: Rate limited to ~1 req/sec, requires User-Agent. Consider server-side geocoding or paid provider for production.

countriesnow.space: Large dataset (~250 countries, thousands of cities) matched client-side. Consider caching to reduce repeated fetches.

Screenshot

UI unchanged, maintains existing search/selection flow:

Homepage

Original prompt

Problem

The search/autosuggest in assets/js/location-search.js currently calls a Netlify function (strong-mermaid preview URL) to resolve city/postcode to coordinates. That function is not available for all deployments and we want to replace the remote endpoint with the public countries list endpoint at https://countriesnow.space/api/v0.1/countries and adjust the code to work with the returned JSON.

Goal

Replace the existing getCoordinates flow so the client fetches the countries data from https://countriesnow.space/api/v0.1/countries, matches user input against country and city names, displays matches to the user, and resolves a chosen location to lat/lon using a geocoding service. Maintain existing UX: if a single match is found, navigate to the weather page with coordinates; if multiple matches are found, show choices. Keep error handling and messaging consistent with the rest of the file.

Files to change

  • assets/js/location-search.js

Detailed changes

  1. Replace the implementation of getCoordinates so it:

    • Fetches https://countriesnow.space/api/v0.1/countries
    • Parses the returned JSON (response JSON has a top-level data array of objects with country and cities)
    • Matches the user query (case-insensitive substring match) against country names and city names
    • Returns an array of match objects with the shape { country, city, display }
    • Deduplicates matches by display label
  2. Add a helper function geocodeLocation(city, country) which:

    • Uses Nominatim (OpenStreetMap) to geocode a city+country or country-only query
    • Returns { lat, lon } on success or null on failure
    • Logs errors and respects Nominatim usage notes in comments
  3. Update handleSearch to:

    • Call the new getCoordinates
    • If no items, show "No locations found..."
    • If exactly one item, geocode it and navigate to the weather page
    • If multiple, call renderChoices(items)
  4. Update renderChoices to render the new match objects and when a user selects one, geocode it then navigate to the weather page. Preserve existing DOM structure/classes used for accessibility and styling.

  5. Add comments noting that countriesnow returns a list without coordinates and that Nominatim has rate limits and usage terms; recommend a server-side or API-key provider for production.

Replacement code (drop-in for the existing getCoordinates / handleSearch / renderChoices and added helper):

// Fetch list of countries and cities and return matches based on the user's query.
// Returns an array of { country, city, display } objects (city may be null)
const getCoordinates = async () => {
const cityOrPostcode = getCityOrPostcode();
if (!cityOrPostcode) return null;

try {
const resp = await fetch("https://countriesnow.space/api/v0.1/countries");
const json = await resp.json();
if (!json || !json.data) return null;

const q = cityOrPostcode.trim().toLowerCase();
const matches = [];

json.data.forEach(({ country, cities }) => {
  if (!country) return;

  // if the query matches the country name, add country + its cities (if any)
  if (country.toLowerCase().includes(q)) {
    if (Array.isArray(cities) && cities.length) {
      cities.forEach((city) =>
        matches.push({ country, city, display: `${city}, ${country}` })
      );
    } else {
      matches.push({ country, city: null, display: country });
    }
    return;
  }

  // otherwise check individual city names
  if (Array.isArray(cities)) {
    cities.forEach((city) => {
      if (city.toLowerCase().includes(q)) {
        matches.push({ country, city, display: `${city}, ${country}` });
      }
    });
  }
});

// Deduplicate on display label
const deduped = Array.from(
  new Map(matches.map((m) => [m.display.toLowerCase(), m])).values()
);

return deduped;

} catch (error) {
console.error("Error fetching countries list:", error);
throw error;
}
};

// Lightweight geocoding using Nominatim (OpenStreetMap). Returns { lat, lon } or null.
// Note: Nominatim has usage terms/rate limits. For production use, consider a server-side
// geocode service or an API key-based provider.
const geocodeLocation = async (city, country) => {
const q = city ? ${city}, ${country} : country;
const url = https://nominatim.openstreetmap.org/search?format=json&limit=1&q=${encodeURIComponent( q )};

try {
const res = await fetch(url, {
headers: {
Accept: "application/json",
},
});
const data = await res.json();
if (Array.isArray(data) && data.length > 0) {
return { lat: parseFloat(data[0].lat), lon: parseFloat(data[0].lon) };
}
return null;
} catch (err) {
console.error("Geocode error:", err);
return null;
}
};

const handleSearch = async () => {
btn.disabled = true;
clearResults();
try {
const items = await getCoordinates();

if (!items || items.length...

This pull request was created as a result of the following prompt from Copilot chat.

Problem

The search/autosuggest in assets/js/location-search.js currently calls a Netlify function (strong-mermaid preview URL) to resolve city/postcode to coordinates. That function is not available for all deployments and we want to replace the remote endpoint with the public countries list endpoint at https://countriesnow.space/api/v0.1/countries and adjust the code to work with the returned JSON.

Goal

Replace the existing getCoordinates flow so the client fetches the countries data from https://countriesnow.space/api/v0.1/countries, matches user input against country and city names, displays matches to the user, and resolves a chosen location to lat/lon using a geocoding service. Maintain existing UX: if a single match is found, navigate to the weather page with coordinates; if multiple matches are found, show choices. Keep error handling and messaging consistent with the rest of the file.

Files to change

  • assets/js/location-search.js

Detailed changes

  1. Replace the implementation of getCoordinates so it:

    • Fetches https://countriesnow.space/api/v0.1/countries
    • Parses the returned JSON (response JSON has a top-level data array of objects with country and cities)
    • Matches the user query (case-insensitive substring match) against country names and city names
    • Returns an array of match objects with the shape { country, city, display }
    • Deduplicates matches by display label
  2. Add a helper function geocodeLocation(city, country) which:

    • Uses Nominatim (OpenStreetMap) to geocode a city+country or country-only query
    • Returns { lat, lon } on success or null on failure
    • Logs errors and respects Nominatim usage notes in comments
  3. Update handleSearch to:

    • Call the new getCoordinates
    • If no items, show "No locations found..."
    • If exactly one item, geocode it and navigate to the weather page
    • If multiple, call renderChoices(items)
  4. Update renderChoices to render the new match objects and when a user selects one, geocode it then navigate to the weather page. Preserve existing DOM structure/classes used for accessibility and styling.

  5. Add comments noting that countriesnow returns a list without coordinates and that Nominatim has rate limits and usage terms; recommend a server-side or API-key provider for production.

Replacement code (drop-in for the existing getCoordinates / handleSearch / renderChoices and added helper):

// Fetch list of countries and cities and return matches based on the user's query.
// Returns an array of { country, city, display } objects (city may be null)
const getCoordinates = async () => {
const cityOrPostcode = getCityOrPostcode();
if (!cityOrPostcode) return null;

try {
const resp = await fetch("https://countriesnow.space/api/v0.1/countries");
const json = await resp.json();
if (!json || !json.data) return null;

const q = cityOrPostcode.trim().toLowerCase();
const matches = [];

json.data.forEach(({ country, cities }) => {
  if (!country) return;

  // if the query matches the country name, add country + its cities (if any)
  if (country.toLowerCase().includes(q)) {
    if (Array.isArray(cities) && cities.length) {
      cities.forEach((city) =>
        matches.push({ country, city, display: `${city}, ${country}` })
      );
    } else {
      matches.push({ country, city: null, display: country });
    }
    return;
  }

  // otherwise check individual city names
  if (Array.isArray(cities)) {
    cities.forEach((city) => {
      if (city.toLowerCase().includes(q)) {
        matches.push({ country, city, display: `${city}, ${country}` });
      }
    });
  }
});

// Deduplicate on display label
const deduped = Array.from(
  new Map(matches.map((m) => [m.display.toLowerCase(), m])).values()
);

return deduped;

} catch (error) {
console.error("Error fetching countries list:", error);
throw error;
}
};

// Lightweight geocoding using Nominatim (OpenStreetMap). Returns { lat, lon } or null.
// Note: Nominatim has usage terms/rate limits. For production use, consider a server-side
// geocode service or an API key-based provider.
const geocodeLocation = async (city, country) => {
const q = city ? ${city}, ${country} : country;
const url = https://nominatim.openstreetmap.org/search?format=json&limit=1&q=${encodeURIComponent( q )};

try {
const res = await fetch(url, {
headers: {
Accept: "application/json",
},
});
const data = await res.json();
if (Array.isArray(data) && data.length > 0) {
return { lat: parseFloat(data[0].lat), lon: parseFloat(data[0].lon) };
}
return null;
} catch (err) {
console.error("Geocode error:", err);
return null;
}
};

const handleSearch = async () => {
btn.disabled = true;
clearResults();
try {
const items = await getCoordinates();

if (!items || items.length === 0) {
  showMessage("No locations found. Please try a different query.");
  return;
}

// If exactly one match, attempt to geocode immediately and load weather page
if (items.length === 1) {
  const only = items[0];
  const coords = await geocodeLocation(only.city, only.country);
  if (coords) {
    loadWeatherPage(coords.lat, coords.lon);
    return;
  } else {
    showMessage("Found location but couldn't resolve coordinates.");
    return;
  }
}

// Multiple matches: render choices for the user to pick
renderChoices(items);

} catch (err) {
console.error("Lookup failed:", err);
showMessage("Lookup failed. Please try again.");
} finally {
btn.disabled = false;
}
};

const renderChoices = (items) => {
clearResults();
if (!items || items.length === 0) return;

const list = document.createElement("ul");
list.className = "search-results-list";

items.forEach((location) => {
const li = document.createElement("li");
li.className = "search-results-item";

const choiceBtn = document.createElement("button");
choiceBtn.type = "button";
choiceBtn.className = "search-result-btn btn btn-sm";
choiceBtn.textContent = location.display;

// When user chooses an item, geocode and navigate to weather page
choiceBtn.addEventListener("click", async () => {
  // disable this button while resolving
  choiceBtn.disabled = true;
  showMessage("Resolving location...");
  try {
    const coords = await geocodeLocation(location.city, location.country);
    if (coords) {
      loadWeatherPage(coords.lat, coords.lon);
    } else {
      showMessage("Unable to resolve chosen location to coordinates.");
    }
  } catch (err) {
    console.error("Choice resolution failed:", err);
    showMessage("Failed to resolve location. Please try another.");
  } finally {
    choiceBtn.disabled = false;
  }
});

li.appendChild(choiceBtn);
list.appendChild(li);

});

resultsContainer.appendChild(list);
resultsContainer.classList.add("open");
};

Notes

  • The countriesnow endpoint returns country + city names but not coordinates; the code uses Nominatim to resolve coordinates for selected items. Nominatim has rate limits and a usage policy — for production consider server-side geocoding or an API-key provider.
  • The countriesnow dataset is large; matching is done in-memory and results may be truncated. If you need better performance or more advanced matching, consider a server-side search endpoint.

Please create a pull request that updates assets/js/location-search.js with the changes above, preserving the rest of the file and project structure. The PR should include a descriptive title and body describing why the change was made and the implications (Nominatim usage notes).


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits November 21, 2025 15:39
Co-authored-by: niraj-sachania <39126181+niraj-sachania@users.noreply.github.com>
Co-authored-by: niraj-sachania <39126181+niraj-sachania@users.noreply.github.com>
Copilot AI changed the title [WIP] Replace city/postcode resolution with countries API data Replace Netlify function with public APIs for location search Nov 21, 2025
Copilot AI requested a review from niraj-sachania November 21, 2025 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants