GeoNames API in C#

by Cyrus Gomes

GeoNames API documentation: https://www.geonames.org/export/web-services.html

GeoNames API license: https://www.geonames.org/export/

The GeoNames API is licensed under the Creative Commons’ CC 4.0 license, allowing users to share and adapt the API’s data for any purpose, as long as appropriate attribution is given.

These recipe examples were tested on August 21, 2024.

NOTE: The GeoNames API limits users to a maximum of 10000 credits per day and 1000 requests per hour. See here for a list of how many credits a request to each endpoint uses.

Setup#

First, install the CURL and jq packages by typing the following command in the terminal:

!sudo apt install curl jq

Create a directory for the GeoNames project:

!mkdir GeoNames

Now, change to the directory we created:

%cd GeoNames

User Registration#

Users must register with GeoNames before accessing the GeoNames API. Sign up can be found here: https://www.geonames.org/login

Add your username in the textfile we create below.

# Create the key file
!touch "username.txt"

Use the following command to access the key as Jupyter does not allow variable sharing for bash scripts.

# Input the key into the file by copy/paste or keying in manually
# Read the key from the file
!username=$(cat "username.txt")

We use the %%file command to create the following makefile which will compile our program and create an executable.

%%file makefile

# Set the variable CC to gcc, which is used to build the program
CC=gcc

# Enable debugging information and enable all compiler warnings
CFLAGS=-g -Wall

# Set the bin variable as the name of the binary file we are creating
BIN=geonames

# Create the binary file with the name we put
all: $(BIN)

# Map any file ending in .c to a binary executable. 
# "$<" represents the .c file and "$@" represents the target binary executable
%: %.c

	# Compile the .c file using the gcc compiler with the CFLAGS and links 
	# resulting binary with the CURL library
	$(CC) $(CFLAGS) $< -o $@ -lcurl

# Clean target which removes specific files
clean:

	# Remove the binary file and an ".dSYM" (debug symbols for debugging) directories
	# the RM command used -r to remove directories and -f to force delete
	$(RM) -rf $(BIN) *.dSYM
Writing makefile

This command is used again to create our .c file which contains the code for the program

%%file ./geonames.c

#include <curl/curl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* CURL program that retrieves GeoNames data from
   https://secure.geonames.org/ */

int main(int argc, char *argv[]) {
    
    // Exit if arguments are invalid
    if (argc != 3 || strcmp(argv[1], "-url") != 0) {
        fprintf(stderr, "Error. Please provide the URL correctly. (./geonames -url [url])\n");
        return EXIT_FAILURE;
    }

    // Algorithm to encode the URL
    // Replace spaces with "%20" in the URL
    char *url = argv[2];
    int len = strlen(url);
    char *encoded_url = malloc(len * 3 + 1); // Allocate enough space for %20 replacements
    if (!encoded_url) {
        fprintf(stderr, "Memory allocation failed\n");
        return EXIT_FAILURE;
    }

    int j = 0;
    for (int i = 0; i < len; ++i) {
        if (url[i] == ' ') {
            encoded_url[j++] = '%';
            encoded_url[j++] = '2';
            encoded_url[j++] = '0';
        } else {
            encoded_url[j++] = url[i];
        }
    }
    encoded_url[j] = '\0'; // null-terminate the string

    // Initialize CURL HTTP connection
    CURL *curl = curl_easy_init();
    if (!curl) {
        fprintf(stderr, "CURL initialization failed\n");
        free(encoded_url);
        return EXIT_FAILURE;
    }

    // Set the URL to which the HTTP request will be sent
    curl_easy_setopt(curl, CURLOPT_URL, encoded_url);

    // Set option to follow redirections
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);

    // Perform the HTTP request
    CURLcode result = curl_easy_perform(curl);

    // Check if the request was successful
    if (result != CURLE_OK) {
        fprintf(stderr, "Download problem: %s\n", curl_easy_strerror(result));
    }

    // Debugging: Print the response code to stderr
    long response_code;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
    fprintf(stderr, "Response code: %ld\n", response_code);

    // Cleanup and deallocate resources
    curl_easy_cleanup(curl);
    free(encoded_url);
    return EXIT_SUCCESS;
}
Writing ./geonames.c
!make
# Compile the .c file using the gcc compiler with the CFLAGS and links 
# resulting binary with the CURL library
gcc -g -Wall geonames.c -o geonames -lcurl

1. Searching with a ZIP code#

This example uses the postalCodeSearchJSON endpoint to find the coordinates of the the ZIP code 35401.

%%bash

# Define endpoint and retrieve username
endpoint="postalCodeSearchJSON"
username=$(< "username.txt")

# Construct parameters array
parameters=(
    "postalcode=35401"   # Postal code to search
    "countryBias=US"     # Moves US results to the top of the results list
    "username=$username" # Must include GeoNames username in all API calls
)

# Join array elements using '&' as separator
parameters_string=$(IFS='&'; echo "${parameters[*]}")

# Retrieve the first index of the postal code
./geonames -url "https://secure.geonames.org/$endpoint?$parameters_string" | jq '.' > response.json
Response code: 200
# Retrieve the response and parses the latitude and longitude
!cat response.json | jq '.["postalCodes"][0] | "\(.lat), \(.lng)"'
"33.196891, -87.562666"

2. Searching with a queries#

Queries allow users to search for location at several different levels.

Searching for a city#

In this example, we search for a location using the query “Tuscaloosa.”

%%bash

# Define endpoint and retrieve username
endpoint="searchJSON"
username=$(< "username.txt")

# Construct parameters array
parameters=(
    "q=Tuscaloosa"          # Postal code to search
    "countryBias=US"        # Moves US results to the top of the results list
    "maxRows=10"            # Limit results to top 10
    "username=$username"    # Must include GeoNames username in all API calls
)

# Join array elements using '&' as separator
parameters_string=$(IFS='&'; echo "${parameters[*]}")

# Retrieve the first index of the postal code
./geonames -url "https://secure.geonames.org/$endpoint?$parameters_string" | jq '.' > response2.json
Response code: 200
# Display top result
!cat response2.json | jq '.["geonames"][0]'
{
  "adminCode1": "AL",
  "lng": "-87.56917",
  "geonameId": 4094455,
  "toponymName": "Tuscaloosa",
  "countryId": "6252001",
  "fcl": "P",
  "population": 98332,
  "countryCode": "US",
  "name": "Tuscaloosa",
  "fclName": "city, village,...",
  "adminCodes1": {
    "ISO3166_2": "AL"
  },
  "countryName": "United States",
  "fcodeName": "seat of a second-order administrative division",
  "adminName1": "Alabama",
  "lat": "33.20984",
  "fcode": "PPLA2"
}

Seaching for a buidling#

In this example, we search for a location using the query “Bruno Business Library.”

%%bash

# Define endpoint and retrieve username
endpoint="searchJSON"
username=$(< "username.txt")

# Construct parameters array
parameters=(
    "q=Bruno Business Library"  # Search query
    "countryBias=US"            # Moves US results to the top of the results list
    "maxRows=10"                # Limit results to top 10
    "username=$username"        # Must include GeoNames username in all API calls
)

# Join array elements using '&' as separator
parameters_string=$(IFS='&'; echo "${parameters[*]}")

./geonames -url "https://secure.geonames.org/$endpoint?$parameters_string" | jq '.' > response3.json
Response code: 200
# Display top result
!cat response3.json | jq '.["geonames"][0]'
{
  "adminCode1": "AL",
  "lng": "-87.54925",
  "geonameId": 11524498,
  "toponymName": "Angelo Bruno Business Library",
  "countryId": "6252001",
  "fcl": "S",
  "population": 0,
  "countryCode": "US",
  "name": "Angelo Bruno Business Library",
  "fclName": "spot, building, farm",
  "adminCodes1": {
    "ISO3166_2": "AL"
  },
  "countryName": "United States",
  "fcodeName": "library",
  "adminName1": "Alabama",
  "lat": "33.2111",
  "fcode": "LIBR"
}

Searching for an island#

In this example, we use the query “Martha’s Vineyard.”

%%bash

# Define endpoint and retrieve username
endpoint="searchJSON"
username=$(< "username.txt")

# Construct parameters array
parameters=(
    "q=Martha's Vineyard"       # Search query
    "countryBias=US"            # Moves US results to the top of the results list
    "maxRows=10"                # Limit results to top 10
    "username=$username"        # Must include GeoNames username in all API calls
)

# Join array elements using '&' as separator
parameters_string=$(IFS='&'; echo "${parameters[*]}")

./geonames -url "https://secure.geonames.org/$endpoint?$parameters_string" | jq '.' > response4.json
Response code: 200
# Display top result
!cat response4.json | jq '.["geonames"][0]'
{
  "adminCode1": "MA",
  "lng": "-70.61265",
  "geonameId": 4943237,
  "toponymName": "Martha's Vineyard Airport",
  "countryId": "6252001",
  "fcl": "S",
  "population": 0,
  "countryCode": "US",
  "name": "Martha's Vineyard Airport",
  "fclName": "spot, building, farm",
  "adminCodes1": {
    "ISO3166_2": "MA"
  },
  "countryName": "United States",
  "fcodeName": "airport",
  "adminName1": "Massachusetts",
  "lat": "41.39016",
  "fcode": "AIRP"
}

Note that the result above is the data for Matha’s Vineyard Airport. If we wish to find the data associated with the island, we can look at the fcodeName of the locations in the response:

!jq -r '.geonames[] | "\(.toponymName) \(40 - (.toponymName | length) | " " * .) \(.fcodeName)"' response4.json
Martha's Vineyard Airport                 airport
Martha's Vineyard Island                  island
Vineyard Haven                            populated place
Martha's Vineyard Hospital                hospital
Martha's Vineyard Regional High School    school
Marthas Vineyard Campground               camp(s)
Martha's Vineyard Aero Light              
Martha's Vineyard State Forest            forest(s)
Martha's Vineyard Agricultural Society    vineyard
Martha's Vineyard State Forest            forest(s)

3. Reverse Geocoding#

The findNearbyPostalCodesJSON endpoint can be used to find the ZIP code of a pair of coordinates.

%%bash

# Define endpoint and retrieve username
endpoint="findNearbyPostalCodesJSON"
username=$(< "username.txt")

# Construct parameters array
parameters=(
    "lat=38.625189"             # Search latitude
    "lng=-90.187330"            # Search longitude
    "maxRows=10"                # Limit results to top 10
    "username=$username"        # Must include GeoNames username in all API calls
)

# Join array elements using '&' as separator
parameters_string=$(IFS='&'; echo "${parameters[*]}")

./geonames -url "https://secure.geonames.org/$endpoint?$parameters_string" | jq '.' > response5.json
Response code: 200
%%bash

# Print header
echo "ZIP     | Distance (km)"

# Print the postal codes and distance
jq -r '.postalCodes[] | "\(.postalCode) \(6 - (.postalCode | length) | " " * .) | \(.distance)"' response5.json
ZIP     | Distance (km)
63102   | 0
63188   | 0.94603
63197   | 0.94603
63180   | 0.94603
63169   | 0.94603
63182   | 0.94603
63150   | 0.94603
63155   | 0.94603
63101   | 1.1038
62202   | 2.64737