Skip to content
This repository has been archived by the owner on Mar 18, 2021. It is now read-only.

Commit

Permalink
Upgrade to v3 of Google's geocoding API
Browse files Browse the repository at this point in the history
  • Loading branch information
mcmire committed Dec 19, 2011
1 parent e5c038a commit d3842ae
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 86 deletions.
6 changes: 6 additions & 0 deletions Gemfile.lock
Expand Up @@ -2,17 +2,23 @@ PATH
remote: .
specs:
address-standardization (0.4.1)
httparty
mechanize (~> 2.0.1)

GEM
remote: http://rubygems.org/
specs:
diff-lcs (1.1.3)
httparty (0.8.1)
multi_json
multi_xml
mechanize (2.0.1)
net-http-digest_auth (~> 1.1, >= 1.1.1)
net-http-persistent (~> 1.8)
nokogiri (~> 1.4)
webrobots (~> 0.0, >= 0.0.9)
multi_json (1.0.4)
multi_xml (0.4.1)
net-http-digest_auth (1.2)
net-http-persistent (1.9)
nokogiri (1.5.0)
Expand Down
30 changes: 15 additions & 15 deletions README.md
Expand Up @@ -27,7 +27,7 @@ MelissaData provides two services itself: [US address lookup](http://www.melissa
:city => "Cupertino",
:state => "CA"
)

This submits the address to MelissaData. If the address can't be found, you'll get back `nil`. But if the address can be found (as in this case), you'll get an instance of `AddressStandardization::Address`. If you store the instance, you can refer to the individual fields like so:

addr.street #=> "1 INFINITE LOOP"
Expand Down Expand Up @@ -61,26 +61,26 @@ Using Google Maps to validate an address is just as easy:
:city => "Mountain View",
:state => "CA"
)
addr.street #=> "1600 AMPHITHEATRE PKWY"
addr.city #=> "MOUNTAIN VIEW"
addr.street #=> "1600 Amphitheatre Pkwy",
addr.city #=> "Mountain View"
addr.county #=> "Santa Clara"
addr.state #=> "CA"
addr.zip #=> "94043"
addr.country #=> "USA"
addr.country #=> "United States"

And, again, a Canadian address:

addr = AddressStandardization::GoogleMaps.standardize_address(
:street => "1770 Stenson Blvd.",
:city => "Peterborough",
:province => "ON"
:street => "55 East Cordova St. Apt 415",
:city => "Vancouver",
:province => "BC"
)
addr.street #=> "1770 STENSON BLVD"
addr.city #=> "PETERBOROUGH"
addr.street #=> "55 E Cordova St"
addr.city #=> "Vancouver"
addr.county #=> "Greater Vancouver Regional District"
addr.province #=> "ON"
addr.postalcode #=> "K9K"
addr.country #=> "CANADA"

Sharp eyes will notice that the Google Maps API doesn't return the full postal code for Canadian addresses. If you know why this is please let me know (my email address is below).
addr.postalcode #=> "V6A 1K3"
addr.country #=> "Canada"

## Support

Expand All @@ -92,4 +92,4 @@ If you find any bugs with this plugin, feel free to:

## Author/License

(c) 2008-2010 Elliot Winkler. Released under the MIT license.
(c) 2008-2010 Elliot Winkler. Released under the MIT license.
1 change: 1 addition & 0 deletions address_standardization.gemspec
Expand Up @@ -16,6 +16,7 @@ Gem::Specification.new do |gem|
gem.version = AddressStandardization::VERSION

gem.add_runtime_dependency('mechanize', '~> 2.0.1')
gem.add_runtime_dependency('httparty', '~> 0.8.1')

gem.add_development_dependency('rspec', '~> 2.7.0')
end
24 changes: 14 additions & 10 deletions lib/address_standardization.rb
@@ -1,28 +1,32 @@
# address_standardization: A tiny Ruby library to quickly standardize a postal address.
# Copyright (C) 2008-2010 Elliot Winkler. Released under the MIT license.

# TODO: Force users to require MelissaData or GoogleMaps manually
# so that no dependency is required without being unused
require 'mechanize'
require 'httparty'

require 'address_standardization/ruby_ext'
require 'address_standardization/class_level_inheritable_attributes'

require 'address_standardization/address'
require 'address_standardization/abstract_service'
require 'address_standardization/melissa_data'
require 'address_standardization/google_maps'
here = File.expand_path('..', __FILE__)
require "#{here}/address_standardization/ruby_ext"
require "#{here}/address_standardization/class_level_inheritable_attributes"

require "#{here}/address_standardization/address"
require "#{here}/address_standardization/abstract_service"
require "#{here}/address_standardization/melissa_data"
require "#{here}/address_standardization/google_maps"

module AddressStandardization
class << self
attr_accessor :test_mode
alias_method :test_mode?, :test_mode

attr_accessor :debug_mode
alias_method :debug_mode?, :debug_mode

def debug(*args)
puts(*args) if debug_mode?
end
end
self.test_mode = false
self.debug_mode = $DEBUG || ENV["DEBUG"] || false
end
end
22 changes: 11 additions & 11 deletions lib/address_standardization/address.rb
@@ -1,16 +1,16 @@
# TODO: Rename to address.rb

module AddressStandardization
class StandardizationError < StandardError; end


# TODO: Rewrite this class so keys are attr_accessorized
# TODO: Alias county to district
class Address
class << self
attr_accessor :valid_keys
end
self.valid_keys = %w(street city state province zip postalcode country county)

attr_reader :address_info

def initialize(address_info)
raise NotImplementedError, "You must define valid_keys" unless self.class.valid_keys
raise ArgumentError, "No address given!" if address_info.empty?
Expand All @@ -19,15 +19,15 @@ def initialize(address_info)
standardize_values!(address_info)
@address_info = address_info
end

def validate_keys(hash)
# assume keys are already stringified
invalid_keys = hash.keys - self.class.valid_keys
unless invalid_keys.empty?
raise ArgumentError, "Invalid keys: #{invalid_keys.join(', ')}. Valid keys are: #{self.class.valid_keys.join(', ')}"
end
end

def method_missing(name, *args)
name = name.to_s
if self.class.valid_keys.include?(name)
Expand All @@ -40,18 +40,18 @@ def method_missing(name, *args)
super(name.to_sym, *args)
end
end

def ==(other)
other.kind_of?(self.class) && @address_info == other.address_info
end

private
def standardize_values!(hash)
hash.each {|k,v| hash[k] = standardize_value(v) }
end

def standardize_value(value)
value ? value.strip_whitespace : ""
end
end
end
end
103 changes: 63 additions & 40 deletions lib/address_standardization/google_maps.rb
@@ -1,55 +1,78 @@
module AddressStandardization
# See <http://code.google.com/apis/maps/documentation/geocoding/>
# See http://code.google.com/apis/maps/documentation/geocoding/
# for documentation on Google's Geocoding API.
class GoogleMaps < AbstractService
class << self
attr_accessor :api_key

def api_key; end
def api_key=(value)
warn "The Google Maps API no longer requires a key, so you are free to remove `AddressStandardization::GoogleMaps.api_key = ...` from your code as it is now a no-op."
end

protected
# much of this code was borrowed from GeoKit, thanks...
def get_live_response(address_info)
raise "API key not specified.\nCall AddressStandardization::GoogleMaps.api_key = '...' before you call .standardize()." unless GoogleMaps.api_key

# Much of this code was borrowed from GeoKit, specifically:
# https://github.com/andre/geokit-gem/blob/master/lib/geokit/geocoders.rb#L530

address_info = address_info.stringify_keys

address_str = [
address_info["street"],
address_info["city"],
(address_info["state"] || address_info["province"]),
address_info["zip"]
].compact.join(" ")
url = "http://maps.google.com/maps/geo?q=#{address_str.url_escape}&output=xml&key=#{GoogleMaps.api_key}&oe=utf-8"
AddressStandardization.debug "[GoogleMaps] Hitting URL: #{url}"
uri = URI.parse(url)
res = Net::HTTP.get_response(uri)
return unless res.is_a?(Net::HTTPSuccess)

content = res.body
AddressStandardization.debug "[GoogleMaps] Response body:"
AddressStandardization.debug "--------------------------------------------------"
AddressStandardization.debug content
AddressStandardization.debug "--------------------------------------------------"
xml = Nokogiri::XML(content)
xml.remove_namespaces! # good or bad? I say good.
return unless xml.at("/kml/Response/Status/code").inner_text == "200"

addr = {}

addr[:street] = get_inner_text(xml, '//ThoroughfareName').to_s
addr[:city] = get_inner_text(xml, '//LocalityName').to_s
addr[:province] = addr[:state] = get_inner_text(xml, '//AdministrativeAreaName').to_s
addr[:zip] = addr[:postalcode] = get_inner_text(xml, '//PostalCodeNumber').to_s
addr[:country] = get_inner_text(xml, '//CountryName').to_s
addr[:county] = get_inner_text(xml, '//SubAdministrativeAreaName').to_s

return if addr[:street] =~ /^\s*$/ or addr[:city] =~ /^\s*$/

Address.new(addr)
end

private
def get_inner_text(xml, xpath)
lambda {|x| x && x.inner_text.upcase }.call(xml.at(xpath))
address_country = address_info["country"] || "US"

resp = HTTParty.get("http://maps.google.com/maps/api/geocode/json",
:query => {
:sensor => 'false',
:address => address_str,
:bias => address_country.downcase
}
)
data = resp.parsed_response
AddressStandardization.debug <<EOT
[GoogleMaps] Response body:
--------------------------------------------------------------------------------
#{resp.body}
--------------------------------------------------------------------------------
EOT
AddressStandardization.debug <<EOT
[GoogleMaps] Parsed response:
--------------------------------------------------------------------------------
#{data}
--------------------------------------------------------------------------------
EOT

if data['results'].any? && data['status'] == "OK"
result = data['results'].first
addr = {}
street = ["", ""]
result['address_components'].each do |comp|
case
when comp['types'].include?("street_number")
street[0] = comp['short_name']
when comp['types'].include?("route")
street[1] = comp['long_name']
when comp['types'].include?("locality")
addr[:city] = comp['long_name']
when comp['types'].include?("administrative_area_level_1")
addr[:state] = addr[:province] = comp['short_name']
when comp['types'].include?("postal_code")
addr[:postalcode] = addr[:zip] = comp['long_name']
when comp['types'].include?("country")
# addr[:country_code] = comp['short_name']
addr[:country] = comp['long_name']
when comp['types'].include?("administrative_area_level_2")
addr[:county] = comp['long_name']
end
end
addr[:street] = street.join(" ").strip
Address.new(addr)
else
return nil
end
end
end
end
end
end
20 changes: 10 additions & 10 deletions spec/google_maps_spec.rb
@@ -1,7 +1,5 @@
require File.expand_path('../spec_helper', __FILE__)

AddressStandardization::GoogleMaps.api_key = "ABQIAAAALHg3jKnK9wN9K3_ArJA6TxSTZ2OgdK08l2h0_gdsozNQ-6zpaxQvIY84J7Mh1fAHQrYGI4W27qKZaw"

describe AddressStandardization::GoogleMaps do
context 'in production mode' do
before do
Expand All @@ -16,13 +14,14 @@
:state => "CA"
)
addr.should == AddressStandardization::Address.new(
"street" => "1600 AMPHITHEATRE PKWY",
"city" => "MOUNTAIN VIEW",
"street" => "1600 Amphitheatre Pkwy",
"city" => "Mountain View",
"county" => "Santa Clara",
"state" => "CA",
"province" => "CA",
"postalcode" => "94043",
"zip" => "94043",
"country" => "USA"
"country" => "United States"
)
end

Expand All @@ -34,13 +33,14 @@
"province" => "BC"
)
addr.should == AddressStandardization::Address.new(
"street" => "55 CORDOVA ST E #415",
"city" => "VANCOUVER",
"street" => "55 E Cordova St",
"city" => "Vancouver",
"county" => "Greater Vancouver Regional District",
"state" => "BC",
"province" => "BC",
"postalcode" => "V6A",
"zip" => "V6A",
"country" => "CANADA"
"postalcode" => "V6A 1K3",
"zip" => "V6A 1K3",
"country" => "Canada"
)
end

Expand Down

0 comments on commit d3842ae

Please sign in to comment.