diff --git a/Gemfile.lock b/Gemfile.lock index 3359999..02b9772 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/README.md b/README.md index aa04e83..9b5d52a 100644 --- a/README.md +++ b/README.md @@ -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" @@ -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 @@ -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. \ No newline at end of file +(c) 2008-2010 Elliot Winkler. Released under the MIT license. diff --git a/address_standardization.gemspec b/address_standardization.gemspec index c4c61d2..ca7ed78 100644 --- a/address_standardization.gemspec +++ b/address_standardization.gemspec @@ -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 diff --git a/lib/address_standardization.rb b/lib/address_standardization.rb index 2e3047f..ebaa89b 100644 --- a/lib/address_standardization.rb +++ b/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 \ No newline at end of file +end diff --git a/lib/address_standardization/address.rb b/lib/address_standardization/address.rb index f8b3eab..337cc74 100644 --- a/lib/address_standardization/address.rb +++ b/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? @@ -19,7 +19,7 @@ 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 @@ -27,7 +27,7 @@ def validate_keys(hash) 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) @@ -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 \ No newline at end of file +end diff --git a/lib/address_standardization/google_maps.rb b/lib/address_standardization/google_maps.rb index 61cdb4a..bb69f19 100644 --- a/lib/address_standardization/google_maps.rb +++ b/lib/address_standardization/google_maps.rb @@ -1,55 +1,78 @@ module AddressStandardization - # See + # 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 < "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 @@ -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