Skip to content

Commit

Permalink
Add support for optional fields when geocoding
Browse files Browse the repository at this point in the history
This will require a major version bump, as the API has changed: parsing
options hashes out of the splatted arrays was proving way too hairy for
reverse geocoding, so both `geocode` and `reverse_geocode` now expect to
be handed an array at all times and an optional hash of options.

Signed-off-by: David Celis <me@davidcel.is>
  • Loading branch information
davidcelis committed Mar 28, 2014
1 parent c5f8de7 commit b49b3e6
Show file tree
Hide file tree
Showing 18 changed files with 697 additions and 103 deletions.
40 changes: 33 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ geocodio = Geocodio::Client.new

### Geocoding

The `Geocodio::Client#geocode` method is used to request coordinates and expanded information on one or more addresses. It is possible for a geocoding request to yield multiple results with varying degrees of accuracy, so the `geocode` method will always return one `Geocodio::AddressSet` for each query made:
The `Geocodio::Client#geocode` method is used to request coordinates and expanded information on one or more addresses. It accepts an array of addresses and an options hash. If more than one address is provided, `#geocode` will use Geocodio's batch endpoint behind the scenes. It is possible for a geocoding request to yield multiple results with varying degrees of accuracy, so the `geocode` method will always return one `Geocodio::AddressSet` for each query made:

```ruby
results = geocodio.geocode('1 Infinite Loop, Cupertino, CA 95014')
results = geocodio.geocode(['1 Infinite Loop, Cupertino, CA 95014'])
# => #<Geocodio::AddressSet:0x007fdf23a07f80 @query="1 Infinite Loop, Cupertino, CA 95014", @addresses=[...]>
```

Expand All @@ -54,10 +54,10 @@ puts address.longitude # or address.lng
# => -122.03074
```

To perform a batch geocoding operation, simply pass multiple addresses to `Geocodio::Client#geocode`:
To perform a batch geocoding operation as mentioned earlier, simply add more addresses to the passed array:

```ruby
result_sets = geocodio.geocode('1 Infinite Loop, Cupertino, CA 95014', '54 West Colorado Boulevard, Pasadena, CA 91105')
result_sets = geocodio.geocode(['1 Infinite Loop, Cupertino, CA 95014', '54 West Colorado Boulevard, Pasadena, CA 91105'])
# => [#<Geocodio::AddressSet:0x007fdf23a07f80 @query="1 Infinite Loop, Cupertino, CA 95014", @addresses=[...]>, #<Geocodio::AddressSet:0x007fdf23a07f80 @query="54 West Colorado Boulevard, Pasadena, CA 91105", @addresses=[...]>]

cupertino = result_sets.first.best
Expand All @@ -69,17 +69,17 @@ cupertino = result_sets.first.best
The interface to reverse geocoding is very similar to geocoding. Use the `Geocodio::Client#reverse_geocode` method (aliased to `Geocodio::Client#reverse`) with one or more pairs of coordinates:

```ruby
addresses = geocodio.reverse_geocode('37.331669,-122.03074')
addresses = geocodio.reverse_geocode(['37.331669,-122.03074'])
# => #<Geocodio::AddressSet:0x007fdf23a07f80 @query="1 Infinite Loop, Cupertino, CA 95014", @addresses=[...]>

address_sets = geocodio.reverse_geocode('37.331669,-122.03074', '34.145760590909,-118.15204363636')
address_sets = geocodio.reverse_geocode(['37.331669,-122.03074', '34.145760590909,-118.15204363636'])
# => [#<Geocodio::AddressSet:0x007fdf23a07f80 @query="1 Infinite Loop, Cupertino, CA 95014", @addresses=[...]>, #<Geocodio::AddressSet:0x007fdf23a07f80 @query="54 West Colorado Boulevard, Pasadena, CA 91105", @addresses=[...]>]
```

Coordinate pairs can also be specified as hashes:

```ruby
address_sets = geocodio.reverse_geocode({ lat: 37.331669, lng: -122.03074 }, { latitude: 34.145760590909, longitude: -118.15204363636 })
address_sets = geocodio.reverse_geocode([{ lat: 37.331669, lng: -122.03074 }, { latitude: 34.145760590909, longitude: -118.15204363636 }])
# => [#<Geocodio::AddressSet:0x007fdf23a07f80 @query="1 Infinite Loop, Cupertino, CA 95014", @addresses=[...]>, #<Geocodio::AddressSet:0x007fdf23a07f80 @query="54 West Colorado Boulevard, Pasadena, CA 91105", @addresses=[...]>]
```

Expand All @@ -92,6 +92,31 @@ address = geocodio.parse('1 Infinite Loop, Cupertino, CA 95014')

Note that this endpoint performs no geocoding; it merely formats a single provided address according to geocod.io's standards.

### Additional fields

Geocodio has added support for retrieving [additional fields][fields] when geocoding or reverse geocoding. To request these fields, pass an options hash to either `#geocode` or `#reverse_geocode`. Possible fields include `cd` or `cd113`, `stateleg`, `school`, and `timezone`:

```ruby
address = geocodio.geocode(['54 West Colorado Boulevard Pasadena CA 91105'], fields: %w[cd stateleg school timezone])

address.congressional_district
# => #<Geocodio::CongressionalDistrict:0x007fa3c15f41c0 @name="Congressional District 27" @district_number=27 @congress_number=113 @congress_years=2013..2015>

address.house_district
# => #<Geocodio::StateLegislativeDistrict:0x007fa3c15f41c0 @name="Assembly District 41" @district_number=41>

address.senate_district
# => #<Geocodio::StateLegislativeDistrict:0x007fa3c15f41c0 @name="State Senate District 25" @district_number=25>

address.unified_school_district # or .elementary_school_district and .secondary_school_district if not unified
# => #<Geocodio::SchoolDistrict:0x007fa3c15f41c0 @name="Pasadena Unified School District" @lea_code="29940" @grade_low="KG" @grade_high="12">

address.timezone
# => #<Geocodio::Timezone:0x007fa3c15f41c0 @name="PST" @utc_offset=-8 @observes_dst=true>
address.timezone.observes_dst?
# => true
```

## Contributing

1. Fork it ( http://github.com/davidcelis/geocodio/fork )
Expand All @@ -101,3 +126,4 @@ Note that this endpoint performs no geocoding; it merely formats a single provid
5. Create new Pull Request

[geocod.io]: http://geocod.io/
[fields]: http://geocod.io/docs/?ruby#toc_17
55 changes: 41 additions & 14 deletions lib/geocodio/address.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
require 'geocodio/congressional_district'
require 'geocodio/school_district'
require 'geocodio/state_legislative_district'
require 'geocodio/timezone'

module Geocodio
class Address
attr_accessor :number
attr_accessor :predirectional
attr_accessor :street
attr_accessor :suffix
attr_accessor :city
attr_accessor :state
attr_accessor :zip

attr_accessor :latitude
attr_accessor :longitude
attr_reader :number, :predirectional, :street, :suffix, :city, :state, :zip

attr_reader :latitude, :longitude
alias :lat :latitude
alias :lng :longitude

# How accurage geocod.io deemed this result to be given the original query.
attr_reader :congressional_district, :house_district, :senate_district,
:unified_school_district, :elementary_school_district,
:secondary_school_district

attr_reader :timezone

# How accurate geocod.io deemed this result to be given the original query.
#
# @return [Float] a number between 0 and 1
attr_accessor :accuracy
attr_reader :accuracy

def initialize(payload = {})
if payload['address_components']
Expand All @@ -34,9 +38,32 @@ def initialize(payload = {})
@longitude = payload['location']['lng']
end

@accuracy = payload['accuracy']

@accuracy = payload['accuracy']
@formatted_address = payload['formatted_address']

return self unless fields = payload['fields']

if fields['congressional_district'] && !fields['congressional_district'].empty?
@congressional_district = CongressionalDistrict.new(fields['congressional_district'])
end

if fields['state_legislative_districts'] && !fields['state_legislative_districts'].empty?
@house_district = StateLegislativeDistrict.new(fields['state_legislative_districts']['house'])
@senate_district = StateLegislativeDistrict.new(fields['state_legislative_districts']['senate'])
end

if (schools = fields['school_districts']) && !schools.empty?
if schools['unified']
@unified_school_district = SchoolDistrict.new(schools['unified'])
else
@elementary_school_district = SchoolDistrict.new(schools['elementary'])
@secondary_school_district = SchoolDistrict.new(schools['secondary'])
end
end

if fields['timezone'] && !fields['timezone'].empty?
@timezone = Timezone.new(fields['timezone'])
end
end

# Formats the address in the standard way.
Expand Down
49 changes: 29 additions & 20 deletions lib/geocodio/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,17 @@ def initialize(api_key = ENV['GEOCODIO_API_KEY'])
# is submitted to http://api.geocod.io/v1/geocode. Multiple addresses will
# instead submit a POST request.
#
# @param addresses [Array<String>] one or more String addresses
# @param [Array<String>] addresses one or more String addresses
# @param [Hash] options an options hash
# @option options [Array] :fields a list of option fields to request (possible: "cd" or "cd113", "stateleg", "school", "timezone")
# @return [Geocodio::Address, Array<Geocodio::AddressSet>] One or more Address Sets
def geocode(*addresses)
addresses = addresses.first if addresses.first.is_a?(Array)

def geocode(addresses, options = {})
if addresses.size < 1
raise ArgumentError, 'You must provide at least one address to geocode.'
elsif addresses.size == 1
geocode_single(addresses.first)
geocode_single(addresses.first, options)
else
geocode_batch(addresses)
geocode_batch(addresses, options)
end
end

Expand All @@ -50,17 +50,17 @@ def geocode(*addresses)
# http://api.geocod.io/v1/reverse. Multiple pairs of coordinates will
# instead submit a POST request.
#
# @param coordinates [Array<String>, Array<Hash>] one or more pairs of coordinates
# @param [Array<String>, Array<Hash>] coordinates one or more pairs of coordinates
# @param [Hash] options an options hash
# @option options [Array] :fields a list of option fields to request (possible: "cd" or "cd113", "stateleg", "school", "timezone")
# @return [Geocodio::Address, Array<Geocodio::AddressSet>] One or more Address Sets
def reverse_geocode(*coordinates)
coordinates = coordinates.first if coordinates.first.is_a?(Array)

def reverse_geocode(coordinates, options = {})
if coordinates.size < 1
raise ArgumentError, 'You must provide coordinates to reverse geocode.'
elsif coordinates.size == 1
reverse_geocode_single(coordinates.first)
reverse_geocode_single(coordinates.first, options)
else
reverse_geocode_batch(coordinates)
reverse_geocode_batch(coordinates, options)
end
end
alias :reverse :reverse_geocode
Expand All @@ -84,31 +84,40 @@ def parse(address)
end
end

def geocode_single(address)
response = get '/geocode', q: address
def geocode_single(address, options = {})
params = { q: address }
params[:fields] = options[:fields].join(',') if options[:fields]

response = get '/geocode', params
addresses = parse_results(response)

AddressSet.new(address, *addresses)
end

def reverse_geocode_single(pair)
def reverse_geocode_single(pair, options = {})
pair = normalize_coordinates(pair)
params = { q: pair }
params[:fields] = options[:fields].join(',') if options[:fields]

response = get '/reverse', q: pair
response = get '/reverse', params
addresses = parse_results(response)

AddressSet.new(pair, *addresses)
end

def geocode_batch(addresses)
response = post '/geocode', {}, body: addresses
def geocode_batch(addresses, options = {})
options[:fields] = options[:fields].join(',') if options[:fields]

response = post '/geocode', options, body: addresses

parse_nested_results(response)
end

def reverse_geocode_batch(pairs)
def reverse_geocode_batch(pairs, options = {})
pairs.map! { |pair| normalize_coordinates(pair) }
response = post '/reverse', {}, body: pairs
options[:fields] = options[:fields].join(',') if options[:fields]

response = post '/reverse', options, body: pairs

parse_nested_results(response)
end
Expand Down
19 changes: 19 additions & 0 deletions lib/geocodio/congressional_district.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module Geocodio
class CongressionalDistrict
attr_reader :name
attr_reader :district_number
attr_reader :congress_number

def initialize(payload = {})
@name = payload['name']
@district_number = payload['district_number'].to_i
@congress_number = payload['congress_number'].to_i
@congress_years = payload['congress_years']
end

def congress_years
first, last = @congress_years.split('-').map(&:to_i)
first..last
end
end
end
15 changes: 15 additions & 0 deletions lib/geocodio/school_district.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Geocodio
class SchoolDistrict
attr_reader :name
attr_reader :lea_code
attr_reader :grade_low
attr_reader :grade_high

def initialize(payload = {})
@name = payload['name']
@lea_code = payload['lea_code']
@grade_low = payload['grade_low']
@grade_high = payload['grade_high']
end
end
end
11 changes: 11 additions & 0 deletions lib/geocodio/state_legislative_district.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Geocodio
class StateLegislativeDistrict
attr_accessor :name
attr_accessor :district_number

def initialize(payload = {})
@name = payload['name']
@district_number = payload['district_number'].to_i
end
end
end
16 changes: 16 additions & 0 deletions lib/geocodio/timezone.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module Geocodio
class Timezone
attr_reader :name
attr_reader :utc_offset

def initialize(payload = {})
@name = payload['name']
@utc_offset = payload['utc_offset']
@observes_dst = payload['observes_dst']
end

def observes_dst?
!!@observes_dst
end
end
end
2 changes: 1 addition & 1 deletion spec/address_set_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

subject(:address_set) do
VCR.use_cassette('batch_geocode') do
geocodio.geocode(*addresses).last
geocodio.geocode(addresses).last
end
end

Expand Down
38 changes: 37 additions & 1 deletion spec/address_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
context 'when geocoded' do
subject(:address) do
VCR.use_cassette('geocode') do
geocodio.geocode('54 West Colorado Boulevard Pasadena CA 91105').best
geocodio.geocode(['54 West Colorado Boulevard Pasadena CA 91105']).best
end
end

Expand Down Expand Up @@ -101,5 +101,41 @@
it 'has an accuracy' do
expect(address.accuracy).to eq(1)
end

context 'with additional fields' do
subject(:address) do
VCR.use_cassette('geocode_with_fields') do
geocodio.geocode(['54 West Colorado Boulevard Pasadena CA 91105'], fields: %w[cd stateleg school timezone]).best
end
end

it 'has a congressional district' do
expect(address.congressional_district).to be_a(Geocodio::CongressionalDistrict)
end

it 'has a house district' do
expect(address.house_district).to be_a(Geocodio::StateLegislativeDistrict)
end

it 'has a senate district' do
expect(address.senate_district).to be_a(Geocodio::StateLegislativeDistrict)
end

it 'has a unified school district' do
expect(address.unified_school_district).to be_a(Geocodio::SchoolDistrict)
end

it 'could have an elementary school district' do
expect(address.elementary_school_district).to be_nil
end

it 'could have a secondary school district' do
expect(address.secondary_school_district).to be_nil
end

it 'has a timezone' do
expect(address.timezone).to be_a(Geocodio::Timezone)
end
end
end
end
Loading

0 comments on commit b49b3e6

Please sign in to comment.