package Geo::USCensus::Geocoding; use strict; use warnings; use LWP::UserAgent; use JSON; use URI; use Geo::USCensus::Geocoding::Match; =head1 NAME Geo::USCensus::Geocoding - The U.S. Census Bureau geocoding service =head1 VERSION Version 0.01 =cut our $VERSION = '0.01'; our $DEBUG = 0; =head1 SYNOPSIS use Geo::USCensus::Geocoding; my $request = { # required fields street => '123 Main Street', city => 'San Francisco', # city state => 'CA', # state/province zip => '93102', # zip/postal code # optional fields benchmark => 'Public_AR_ACS2013', # default is "Public_AR_Current" vintage => 'Census2010_ACS2013', # default is "Current_Current" debug => 1, # will print the URL and some other info }; my $result = Geo::USCensus::Geocoding->query($request); if ($result->matches) { my $match = $result->match(0); print $match->matchedAddress,"\n", $match->coordinates->{x},',',$match->coordinates->{y},"\n", $match->censustract,"\n"; } =head1 CLASS METHODS =head2 query HASHREF Send a request to the web service. See L for API documentation. This package will always use the JSON data format and the Geographies return type. Returns an object of class Geo::USCensus::Geocoding. =cut my $ua = LWP::UserAgent->new; my $api_uri = 'http://geocoding.geo.census.gov/geocoder/geographies/address'; sub query { my $class = shift; my %opt = ( benchmark => 'Public_AR_Census2010', vintage => 'Census2010_Census2010', ); if (ref $_[0] eq 'HASH') { %opt = (%opt, %{ $_[0] }); } else { %opt = (%opt, @_); } $DEBUG = $opt{debug} || 0; $opt{format} = 'json'; foreach (qw(street city state zip)) { die "$_ required\n" unless length($opt{$_}); } my $uri = URI->new($api_uri); $uri->query_form(\%opt); warn "$class->query\n$uri\n\n" if $DEBUG; my $http_req = HTTP::Request->new(GET => $uri->as_string); my $resp = $ua->request($http_req); my $self = { addr_response => $resp }; bless $self, $class; if ( $resp->is_success ) { local $@; my $tree = eval { from_json($resp->content) }; if ($@) { $self->message("Unable to parse response:\n$@"); return $self; } if (!exists $tree->{result}) { $self->message("Response does not contain geocoding results."); warn $self->message. "\n".$resp->content."\n\n"; return $self; } $tree = $tree->{result}; my @matches; if (exists( $tree->{addressMatches} )) { foreach my $am (@{ $tree->{addressMatches} }) { push @matches, Geo::USCensus::Geocoding::Match->new($am); } } # else what? does this happen if there's no match? a proper REST # interface should throw a 404 $self->{matches} = \@matches; } else { $self->message( $resp->status_line ); } $self; } =head1 METHODS =head2 message Sets/gets an explicit error status. =cut sub message { my $self = shift; if (@_) { $self->{_message} = shift; } $self->{_message} || ''; } =head2 matches Returns the number of matches found. =cut sub matches { my $self = shift; $self->{matches} ? scalar @{ $self->{matches} } : 0; } =head2 match NUMBER Returns a specific match (starting from zero). Matches are returned as L objects, in the order they were returned by the service. =cut sub match { my $self = shift; my $i = shift; $self->{matches}->[$i]; } =head1 AUTHOR Mark Wells, C<< >> =head1 SUPPORT Commercial support for this module is available from Freeside Internet Services: L =back =head1 LICENSE AND COPYRIGHT Copyright (C) 2014 Mark Wells. This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU General Public License as published by the Free Software Foundation; or the Artistic License. See http://dev.perl.org/licenses/ for more information. =cut 1;