Introduction
A recent Dynamics CRM project required U.S. addresses to include their Congressional District name and number. I happened to find three geocoding web services that included the latest legislative information I needed and could be used for free (for the most part). This article describes how to write a console application in C# that can accommodate the different data structures of the JSON response.
Background
There are many geocoding web services available, but APIs provided by the U.S. Census Bureau, LatLon.io and Geocod.io included the Congressional District information I was looking for. While the U.S. Census Bureau web services are completely free and unlimited usage for single address requests, LatLon.io and Geocod.io limit the number of free requests.
Using the Code
All of the source code in the article has been included in the downloadable materials. The demo project was built and tested using Visual Studio 2015 SP3 but there is no reason why the source code shouldn’t work in earlier versions of Visual Studio and .NET 4.5. I used Newtonsoft’s Json.NET to deserialize the JSON responses and is the only package required outside of .NET System assemblies. Download the latest version of JSON.NET at http://www.newtonsoft.com or use the NuGet Package Manager in Visual Studio.
Design
In order to accommodate the different data structures of the JSON responses, separate classes were used for each service (e.g. Census, Geocodio and LatLon). The class properties need to reflect the structure of the JSON response in order to be deserialized.
Census json = JsonConvert.DeserializeObject<Census>(data);
The Geocode object uses a custom Generic for each geocode web service. Instantiating the Census
class looks like this:
var census = new Geocode<Census>();
census.Service = new Census();
var geocodeCensus = census.Service.GeocodeAddress
("1600 Pennsylvania Ave NW", "Washington", "DC", "20500");
Source Code
Geocode.cs
The Geocode class constrains the T
parameter to a Class Type.
Important Note: The SSL/TLS type in the GetResponse()
method is set to SecurityProtocolType.Tls12
or TLS version 1.2. If a security flaw is found and these services upgrade and disable the TLS 1.2 protocol, an error message resembling, "An error occurred while sending the request." will occur.
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
namespace GeocodeConsole
{
internal interface IGeocode
{
HttpResponseMessage GetResponse(string url, string parameters);
}
public class Geocode<T> : IGeocode
where T : class
{
private T service;
public double Latitude { get; set; }
public double Longitude { get; set; }
public string CongressionalDistrictName { get; set; }
public Geocode()
{
}
public T Service
{
get
{
return service;
}
set
{
service = value;
}
}
public HttpResponseMessage GetResponse(string url, string parameters)
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
using (HttpClient client = new HttpClient())
{
client.BaseAddress = new Uri(url);
client.DefaultRequestHeaders.Accept.Add
(new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage response = client.GetAsync(parameters).Result;
return response;
}
}
}
}
Census.cs
The U.S. Census Bureau Geocoding Services are free to use and there are no usage limits, keys or registration. With that said, the API is the slowest among the three. The service is fairly well documented but information on specifics can be hard to find. For more information, go to http://census.gov/data/developers/data-sets/Geocoding-services.html.
Important Note: The Geography
subclass defines an array of CongressionalDistricts
and the JsonProperty
attribute is hard-coded with the name of the current session of Congress (e.g. 115th Congressional Districts).
using Newtonsoft.Json;
using System.Net.Http;
namespace GeocodeConsole
{
internal interface ICensus
{
Geocode<Census> GeocodeAddress
(string street, string city, string state, string zip);
}
public class Census : ICensus
{
private string apiUrl;
private string apiParameters;
public Result result { get; set; }
public Census()
{
apiUrl = @"https://geocoding.geo.census.gov/geocoder/geographies/address";
apiParameters = @"&benchmark=4&vintage=4&format=json&layers=54,86";
}
public class Result
{
public AddressMatches[] addressMatches { get; set; }
}
public class Address
{
public string state { get; set; }
public string street { get; set; }
public string city { get; set; }
}
public class AddressMatches
{
public Geographies geographies { get; set; }
public string matchedAddress { get; set; }
public Coordinates coordinates { get; set; }
public AddressComponents addressComponents { get; set; }
}
public class Geographies
{
[JsonProperty("115th Congressional Districts")]
public CongressionalDistricts[] congressionalDistricts { get; set; }
public Counties[] counties { get; set; }
}
public class Counties
{
public int STATE { get; set; }
public string NAME { get; set; }
public string BASENAME { get; set; }
}
public class CongressionalDistricts
{
public string NAME { get; set; }
public int CDSESSN { get; set; }
public string BASENAME { get; set; }
}
public class Coordinates
{
public double x { get; set; }
public double y { get; set; }
}
public class AddressComponents
{
public string preDirection { get; set; }
public string preType { get; set; }
public string streetName { get; set; }
public string suffixType { get; set; }
public string toAddress { get; set; }
public string preQualifier { get; set; }
public string suffixDirection { get; set; }
public string suffixQualifier { get; set; }
public string fromAddress { get; set; }
public string state { get; set; }
public string zip { get; set; }
public string city { get; set; }
}
public Geocode<Census> GeocodeAddress
(string street, string city = "",
string state = "", string zip = "")
{
Census json = null;
var geocode = new Geocode<Census>();
HttpResponseMessage response = geocode.GetResponse
(this.apiUrl, this.GetUrlParameters(street, city, state, zip));
using (response)
{
if (response.IsSuccessStatusCode)
{
var data = response.Content.ReadAsStringAsync().Result;
json = JsonConvert.DeserializeObject<Census>(data);
if (json != null)
{
geocode.Latitude = json.result.addressMatches[0].coordinates.x;
geocode.Longitude =
json.result.addressMatches[0].coordinates.y;
geocode.CongressionalDistrictName =
json.result.addressMatches[0].geographies.congressionalDistricts[0].NAME;
}
}
return geocode;
}
}
internal string GetUrlParameters
(string street,
string city = "", string state = "", string zip = "")
{
string urlParameters = "";
urlParameters += "?street=" + street;
if (city.Length > 0) urlParameters += "&city=" + city;
if (state.Length > 0) urlParameters += "&state=" + state;
if (zip.Length > 0) urlParameters += "&zip=" + zip;
urlParameters += this.apiParameters;
return urlParameters;
}
}
}
Geocodio.cs
Geocodio requires registration and free geocoding requests are limited to 2,500 requests per day. The service is easy to use and the documentation and examples are helpful. Multiple API keys can be configured and there are no access constraints. Be sure to replace the apiKey value "***API KEY***"
with a valid key.
To get started, go to https://geocod.io:
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Net.Http;
namespace GeocodeConsole
{
internal interface IGeocodio
{
Geocode<Geocodio> GeocodeAddress(string address);
}
class Geocodio : IGeocodio
{
private string apiKey;
private string apiUrl;
private string apiParameters;
public Input input { get; set; }
public List<Result> results { get; set; }
public Geocodio()
{
apiKey = "***API KEY***";
apiUrl = @"https://api.geocod.io/v1/geocode";
apiParameters = "&fields=cd&api_key=" + apiKey;
}
public class AddressComponents
{
public string number { get; set; }
public string street { get; set; }
public string suffix { get; set; }
public string formatted_street { get; set; }
public string city { get; set; }
public string state { get; set; }
public string zip { get; set; }
public string country { get; set; }
}
public class Input
{
public AddressComponents address_components { get; set; }
public string formatted_address { get; set; }
}
public class Location
{
public double lat { get; set; }
public double lng { get; set; }
}
public class CongressionalDistrict
{
public string name { get; set; }
public string district_number { get; set; }
public string congress_number { get; set; }
public string congress_years { get; set; }
}
public class Fields
{
public CongressionalDistrict congressional_district { get; set; }
}
public class Result
{
public AddressComponents address_components { get; set; }
public string formatted_address { get; set; }
public Location location { get; set; }
public double accuracy { get; set; }
public string accuracy_type { get; set; }
public string source { get; set; }
public Fields fields { get; set; }
}
public Geocode<Geocodio> GeocodeAddress(string address)
{
Geocodio json = null;
var geocode = new Geocode<Geocodio>();
HttpResponseMessage response =
geocode.GetResponse(this.apiUrl, this.GetUrlParameters(address));
using (response)
{
if (response.IsSuccessStatusCode)
{
var data = response.Content.ReadAsStringAsync().Result;
json = JsonConvert.DeserializeObject<Geocodio>(data);
if (json != null)
{
geocode.Latitude = json.results[0].location.lat;
geocode.Longitude = json.results[0].location.lng;
geocode.CongressionalDistrictName =
json.results[0].fields.congressional_district.name;
}
}
return geocode;
}
}
internal string GetUrlParameters(string address)
{
string urlParameters = "";
urlParameters += "?q=" + address;
urlParameters += this.apiParameters;
return urlParameters;
}
}
}
LatLon.cs
LatLon requires registration and free geocoding requests are limited to 15,000 requests per month. The service is easy to use and accounts receive a single API token. Be sure to replace the apiKey value "***API KEY***"
with a valid key.
To get started, go to https://latlon.io:
using Newtonsoft.Json;
using System.Net.Http;
namespace GeocodeConsole
{
internal interface ILatLon
{
Geocode<LatLon> GeocodeAddress(string address);
}
public class LatLon : ILatLon
{
private string apiKey;
private string apiUrl;
private string apiParameters;
public double lat { get; set; }
public double lon { get; set; }
public Address address { get; set; }
public Geography geography { get; set; }
public CongressionalDistrict congressional_district { get; set; }
public LatLon()
{
apiKey = "***API KEY***";
apiUrl = @"https://latlon.io/api/v1/geocode";
apiParameters = @"&fields=congressional_district,
geography&token=" + apiKey;
}
public class Address
{
public string address { get; set; }
public string prefix { get; set; }
public string number { get; set; }
public string street_name { get; set; }
public string street_type { get; set; }
public string suffix { get; set; }
public string unit { get; set; }
public string city { get; set; }
public string state { get; set; }
public string zip { get; set; }
}
public class Geography
{
public string state { get; set; }
public string county { get; set; }
public string neighborhood { get; set; }
public Fips fips { get; set; }
}
public class Fips
{
public string state { get; set; }
public string county { get; set; }
public string tract { get; set; }
public int block_group { get; set; }
public int block { get; set; }
}
public class CongressionalDistrict
{
public string name { get; set; }
public int district_number { get; set; }
public string congress_number { get; set; }
}
public Geocode<LatLon> GeocodeAddress(string address)
{
LatLon json = null;
var geocode = new Geocode<LatLon>();
HttpResponseMessage response = geocode.GetResponse
(this.apiUrl, this.GetUrlParameters(address));
using (response)
{
if (response.IsSuccessStatusCode)
{
var data = response.Content.ReadAsStringAsync().Result;
json = JsonConvert.DeserializeObject<LatLon>(data);
if (json != null)
{
geocode.Latitude = json.lat;
geocode.Longitude = json.lon;
geocode.CongressionalDistrictName =
json.congressional_district.name;
}
}
return geocode;
}
}
internal string GetUrlParameters(string address)
{
string urlParameters = "";
urlParameters += "?address=" + address;
urlParameters += this.apiParameters;
return urlParameters;
}
}
}
Program.cs
The main program simply demonstrates the use of all three web services on the same address.
using System;
namespace GeocodeConsole
{
class Program
{
static void Main(string[] args)
{
string street = "350 5th Ave";
string city = "New York";
string state = "NY";
string zip = "10118";
var census = new Geocode<census>();
census.Service = new Census();
var geocodeCensus = census.Service.GeocodeAddress(street, city, state, zip);
if (geocodeCensus != null)
{
Console.WriteLine(geocodeCensus.Latitude + " : " +
geocodeCensus.Longitude + " = " +
geocodeCensus.CongressionalDistrictName);
}
var geocodio = new Geocode<geocodio>();
geocodio.Service = new Geocodio();
var geocodeGeocodio =
geocodio.Service.GeocodeAddress(street + "+" + city + "+" + state + "+" + zip);
if (geocodeGeocodio != null)
{
Console.WriteLine(geocodeGeocodio.Latitude + " : " +
geocodeGeocodio.Longitude + " = " +
geocodeGeocodio.CongressionalDistrictName);
}
var latlon = new Geocode<latlon>();
latlon.Service = new LatLon();
var geocodeLatLon = latlon.Service.GeocodeAddress
(street + "+" + city + "+" + state + "+" + zip);
if (geocodeLatLon != null)
{
Console.WriteLine(geocodeLatLon.Latitude + " : " +
geocodeLatLon.Longitude + " = " +
geocodeLatLon.CongressionalDistrictName);
}
}
}
}
Now What?
In a future article, I will use this project to add the Congressional District name to U.S. addresses in Dynamics CRM.
History