Searching for the Nearest Metro Area

Sagar Luitel 0 Reputation points
2024-09-19T15:28:42.4666667+00:00

Is it possible to use Azure Map to search for the nearest metro area by providing latitude and longitude or an address? For example, if I provide the latitude and longitude for Aurora, Colorado, is it possible to receive Denver, Colorado, as a response? I have tried using POI and Fuzzy search with different queries such as "metro area", "municipality", "downtown", "suburb", and "city", but none of the results were useful. Here is an example query I've tried:

"https://atlas.microsoft.com/search/poi/json?subscription-key={Your-Azure-Maps-Subscription-key}&api-version=1.0&query=metro%20area&limit=3&lat=47.6413362&lon=-122.1327968"
Azure Maps
Azure Maps
An Azure service that provides geospatial APIs to add maps, spatial analytics, and mobility solutions to apps.
716 questions
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. rbrundritt 17,981 Reputation points Microsoft Employee
    2024-09-19T20:06:21.89+00:00

    There is no out of the box service for this type of query. A metro is a locality, just a bigger one, but that distinction isn't in the data. Additionally, there isn't a POI category for this type of entity type. If Aurora was an area within Denver, then a simple reverse geocode would work but that's not the case here. There are a couple of possible options:

    1. Download a list of metro areas from an open data source and then do a search through that. This is likely a fairly cheap solution that wouldn't be overly difficult to create.
    2. Create a grid pattern around your coordinate and do a reverse geocode for each point in the grid and look for results with a different locality value. This wouldn't be overly difficult to create and would mean you have the latest data but wouldn't guarantee that the other locality is larger. This would also be more expensive longer term.

    Looking at option 1, there are couple of ways to define "metro", there is the census statistic area version in the US, but not all countries have this type of area defined. In the US, there are 387 metro statistical areas used by the census (you can see the list here: https://en.wikipedia.org/wiki/Metropolitan_statistical_area). If you only captured just the minimum data you need, most likely city/locality name, state code, lat/lon values, this would be a fairly small amount of data that could be stored easily with an app.

    Looking at a global solution, I would look at population sizes of a city. There are around 28,000 cities in the world that have a population of over 15,000. This list is available as open data from GeoNames here: https://download.geonames.org/export/dump/ (see file cities15000.zip). Unzipped this file is 6MB, however, many data columns could be removed to cut the file size to around 1MB. You could also filter that data more to a higher population minimum if you wanted to. I only mention 15,000 as that is the largest grouping file in GeoNames, but looking at the US metro areas, the smallest population is over 50,000, and there is around 11,000 cities with a population or larger, which would be less than 1MB of data. Parsing and searching this list in code would be very fast (even for the full list).

    I've put together a simple demo of how to do this using JavaScript. The average search time on my computer is 4ms. You can try it out here: https://rbrundritt.azurewebsites.net/Demos/NearbyMetroSearch/index.html

    One thing I noticed is that Aurora, CO actually has a fairly large population, so you might want to sort the results by population and find the right search radius (perhaps 50km). Then use the result with the largest population as your main metro city.

    I limited the data to name, latitude, longitude, countryRegionIso2, adminDistrict1, population columns (also added a header). You can download the file here: https://rbrundritt.azurewebsites.net/Demos/NearbyMetroSearch/cities15000_slim.txt

    Here is the source code:

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <title></title>
        <style>
            .resultsTable {
                border-collapse: collapse;
             }
    
            .resultsTable td, th {
                border: 1px solid black;
                text-align: center;
                padding: 5px;
            }
    
            .resultsTable th, td:nth-child(1) {
                text-align: left;
            }
        </style>
    </head>
    <body>
        <table>
            <tr>
                <td>Latitude</td>
                <td><input id="latInput" type="text" placeholder="Enter latitude value" value="39.754576" /></td>
            </tr>
            <tr>
                <td>Longitude</td>
                <td><input id="lonInput" type="text" placeholder="Enter longitude value" value="-104.666108" /></td>
            </tr>
            <tr>
                <td>Search Radius</td>
                <td>
                    <form oninput="radius.value=searchRadius.value">
                        <input type="range" id="searchRadius" value="100" min="1" max="1000" step="1" title="Search radius" />
                        <output name="radius" for="searchRadius">100</output>km
                    </form>
                </td>
            </tr>
            <tr>
                <td>Min Population</td>
                <td>
                    <form oninput="population.value=minPopulation.value">
                        <input type="range" id="minPopulation" value="15000" min="15000" max="1000000" step="5000" title="Min population" />
                        <output name="population" for="minPopulation">15000</output>
                    </form>
                </td>
            </tr>
            <tr>
                <td>Sort by</td>
                <td>
                    <select id="sort">
                        <option value="distance">Distance (closest to furthest)</option>
                        <option value="population">Population (largest to smallest)</option>
                    </select>
                </td>
            </tr>
            <tr>
                <td></td>
                <td><button onclick="findNearbyMetro()">Find Nearby Metro</button></td>
            </tr>
        </table>
        <br />
        <br />
        <div id="output">Data loading...</div>
    
        <script>        
            var data = [];
    
            //Load city data from a file. File it tab delimited with the following columns: name, latitude, longitude, countryRegionIso2, adminDistrict1, population
            fetch('cities15000_slim.txt').then(r => r.text()).then(r => {
                //Parse the data into an array of objects.
                const lines = r.split('\n');
    
                //Skip the first line (header).
                for (let i = 1; i < lines.length; i++){
                    var parts = lines[i].split('\t');
                    data.push({
                        name: parts[0],
                        latitude: parseFloat(parts[1]),
                        longitude: parseFloat(parts[2]),
                        countryRegionIso2: parts[3],
                        adminDistrict1: parts[4],
                        population: parseInt(parts[5])
                    });
                }
    
                document.getElementById('output').innerHTML = 'Data loaded.';
            });
    
            function findNearbyMetro() {
                const latitude = parseFloat(document.getElementById('latInput').value);
                const longitude = parseFloat(document.getElementById('lonInput').value);
                const searchRadius = parseFloat(document.getElementById('searchRadius').value);
                const minPopulation = parseInt(document.getElementById('minPopulation').value);
                const sortBy = document.getElementById('sort').value;
    
                if(latitude < -90 || latitude > 90){
                    alert('Latitude must be between -90 and 90.');
                    return;
                }
    
                if(longitude < -180 || longitude > 180){
                    alert('Longitude must be between -180 and 180.');
                    return;
                }
    
                const output = document.getElementById('output');
                output.innerHTML = 'Searching...';
    
                const start = Date.now();
    
                //Loop over the data and find the cities that are within the search radius.
                const results = searchCities(latitude, longitude, searchRadius, minPopulation);
    
                //Sort the results.
                if (sortBy === 'population') {
                    //Sort from largest to smallest population.
                    results.sort((a, b) => b[sortBy] - a[sortBy]);
                }
                else if (sortBy === 'distance') {
                    //Sort from shortest to longest distance.
                    results.sort((a, b) => a.distance - b.distance);
                }
    
                const end = Date.now();
    
                //Create a table of the results.
                let html = [`${results.length} results found in ${(end - start)}ms<br/><br/>`];
    
                if (results.length > 0) {
                    html.push('<table class="resultsTable"><tr><th>Name</th><th>AdminDistrict</th><th>CountryRegion Iso2</th><th>Population</th><th>Distance (km)</th></tr>');
                    for (let i = 0; i < results.length; i++) {
                        const city = results[i];
                        html.push(`<tr><td>${city.name}</td><td>${city.adminDistrict1}</td><td>${city.countryRegionIso2}</td><td>${city.population.toLocaleString('en-US')}</td><td>${city.distance.toLocaleString('en-US', { maximumFractionDigits: 2})}</td></tr>`);
                    }
    
                    html.push('</table>');
                }
    
                output.innerHTML = html.join('');
            }
    
            function searchCities(latitude, longitude, searchRadius, minPopulation) {
                //Loop over the data and find the cities that are within the search radius.
                const results = [];
    
                for (let i = 0; i < data.length; i++) {
                    const city = data[i];
    
                    //Skip cities that are below the minimum population.
                    if(city.population < minPopulation){
                        continue;
                    }
    
                    //Calculate distance using haversine formula.
                    const distance = haversineDistance(latitude, longitude, city.latitude, city.longitude);
    
                    if (distance <= searchRadius) {
                        //Clone the result so that we don't accidentally modify the original data upstream, add distance information.
                        results.push(Object.assign({ distance: distance }, city));
                    }
                }
    
                //Sort the results by distance (closest to furthest))
                results.sort((a, b) => a.distance - b.distance);
    
                return results;
            }
    
            //Calculate the distance between two points using the haversine formula.
            //Variations for different programming languages documented here: https://rosettacode.org/wiki/Haversine_formula
            function haversineDistance(lat1, lon1, lat2, lon2) {
                //Convert to radians.
                lat1 = lat1 / 180.0 * Math.PI;
                lon1 = lon1 / 180.0 * Math.PI;
                lat2 = lat2 / 180.0 * Math.PI;
                lon2 = lon2 / 180.0 * Math.PI;
    
                const dLat = lat2 - lat1;
                const dLon = lon2 - lon1;
                const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
                const c = 2 * Math.asin(Math.sqrt(a));
    
                const R = 6372.8; //Earth radius in km
                return R * c;
            }
        </script>
    </body>
    </html>
    
    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.