File Coverage

blib/lib/Geo/Coder/US/Census.pm
Criterion Covered Total %
statement 45 87 51.7
branch 6 32 18.7
condition 1 5 20.0
subroutine 12 15 80.0
pod 5 5 100.0
total 69 144 47.9


line stmt bran cond sub pod time code
1             package Geo::Coder::US::Census;
2              
3 6     6   1401369 use strict;
  6         12  
  6         244  
4 6     6   33 use warnings;
  6         156  
  6         354  
5              
6 6     6   64 use Carp;
  6         15  
  6         461  
7 6     6   3226 use Encode;
  6         108787  
  6         958  
8 6     6   3777 use JSON::MaybeXS;
  6         103830  
  6         803  
9 6     6   3893 use HTTP::Request;
  6         173005  
  6         353  
10 6     6   5378 use LWP::UserAgent;
  6         277080  
  6         359  
11 6     6   4191 use LWP::Protocol::https;
  6         1020518  
  6         460  
12 6     6   66 use URI;
  6         21  
  6         235  
13 6     6   4624 use Geo::StreetAddress::US;
  6         440611  
  6         10301  
14              
15             =head1 NAME
16              
17             Geo::Coder::US::Census - Provides a Geo-Coding functionality for the US using L
18              
19             =head1 VERSION
20              
21             Version 0.07
22              
23             =cut
24              
25             our $VERSION = '0.07';
26              
27             =head1 SYNOPSIS
28              
29             use Geo::Coder::US::Census;
30              
31             my $geo_coder = Geo::Coder::US::Census->new();
32             # Get geocoding results (as a hash decoded from JSON)
33             my $location = $geo_coder->geocode(location => '4600 Silver Hill Rd., Suitland, MD');
34             # Sometimes the server gives a 500 error on this
35             $location = $geo_coder->geocode(location => '4600 Silver Hill Rd., Suitland, MD, USA');
36              
37             =head1 DESCRIPTION
38              
39             Geo::Coder::US::Census provides geocoding functionality specifically for U.S. addresses by interfacing with the U.S. Census Bureau's geocoding service.
40             It allows developers to convert street addresses into geographical coordinates (latitude and longitude) by querying the Census Bureau's API.
41             Using L (or a user-supplied agent), the module constructs and sends an HTTP GET request to the API.
42              
43             The module uses L to break down a given address into its components (street, city, state, etc.),
44             ensuring that the necessary details for geocoding are present.
45              
46             =head1 METHODS
47              
48             =head2 new
49              
50             $geo_coder = Geo::Coder::US::Census->new();
51             my $ua = LWP::UserAgent->new();
52             $ua->env_proxy(1);
53             $geo_coder = Geo::Coder::US::Census->new(ua => $ua);
54              
55             =cut
56              
57             sub new {
58 2     2 1 602599 my $class = $_[0];
59              
60 2         7 shift;
61 2 50       16 my %args = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0         0  
62              
63 2         7 my $ua = $args{ua};
64 2 50       11 if(!defined($ua)) {
65 2         41 $ua = LWP::UserAgent->new(agent => __PACKAGE__ . "/$VERSION");
66 2         8689 $ua->default_header(accept_encoding => 'gzip,deflate');
67 2         173 $ua->env_proxy(1);
68             }
69 2   50     12160 my $host = $args{host} || 'geocoding.geo.census.gov/geocoder/locations/address';
70              
71 2         62 return bless { ua => $ua, host => $host, %args }, $class;
72             }
73              
74             =head2 geocode
75              
76             Geocode an address.
77             It accepts addresses provided in various forms -
78             whether as a single argument, a key/value pair, or within a hash reference -
79             making it easy to integrate into different codebases.
80             It decodes the JSON response from the API using L,
81             providing the result as a hash.
82             This allows easy extraction of latitude, longitude, and other details returned by the service.
83              
84             $location = $geo_coder->geocode(location => $location);
85             # @location = $geo_coder->geocode(location => $location);
86              
87             print 'Latitude: ', $location->{'latt'}, "\n";
88             print 'Longitude: ', $location->{'longt'}, "\n";
89              
90             =cut
91              
92             sub geocode {
93 1     1 1 966 my $self = shift;
94 1         3 my %param;
95              
96 1 50       8 if(ref($_[0]) eq 'HASH') {
    50          
    50          
97 0         0 %param = %{$_[0]};
  0         0  
98             } elsif(ref($_[0])) {
99 0         0 Carp::croak('Usage: geocode(location => $location)');
100             } elsif(@_ % 2 == 0) {
101 1         3 %param = @_;
102             } else {
103 0         0 $param{location} = shift;
104             }
105              
106             my $location = $param{location}
107 1 50       24 or Carp::croak('Usage: geocode(location => $location)');
108              
109 0 0         if (Encode::is_utf8($location)) {
110 0           $location = Encode::encode_utf8($location);
111             }
112              
113 0 0         if($location =~ /,?(.+),\s*(United States|US|USA)$/i) {
114 0           $location = $1;
115             }
116              
117             # Remove county from the string, if that's included
118             # Assumes not more than one town in a state with the same name
119             # in different counties - but the census Geo-Coding doesn't support that
120             # anyway
121             # Some full state names include spaces, e.g South Carolina
122             # Some roads include full stops, e.g. S. West Street
123 0 0         if($location =~ /^(\d+\s+[\w\s\.]+),\s*([\w\s]+),\s*[\w\s]+,\s*([A-Za-z\s]+)$/) {
124 0           $location = "$1, $2, $3";
125             }
126              
127 0           my $uri = URI->new("https://$self->{host}");
128 0           my $hr = Geo::StreetAddress::US->parse_address($location);
129              
130 0 0 0       if((!defined($hr->{'city'})) || (!defined($hr->{'state'}))) {
131             # use Data::Dumper;
132             # print Data::Dumper->new([$hr])->Dump(), "\n";
133 0           Carp::carp(__PACKAGE__ . ": city and state are mandatory ($location)");
134 0           return;
135             }
136              
137             my %query_parameters = (
138             'benchmark' => 'Public_AR_Current',
139             'city' => $hr->{'city'},
140             'format' => 'json',
141 0           'state' => $hr->{'state'},
142             );
143 0 0         if($hr->{'street'}) {
144 0 0         if($hr->{'number'}) {
145 0           $query_parameters{'street'} = $hr->{'number'} . ' ' . $hr->{'street'} . ' ' . $hr->{'type'};
146             } else {
147 0           $query_parameters{'street'} = $hr->{'street'} . ' ' . $hr->{'type'};
148             }
149 0 0         if($hr->{'suffix'}) {
150 0           $query_parameters{'street'} .= ' ' . $hr->{'suffix'};
151             }
152             }
153              
154 0           $uri->query_form(%query_parameters);
155 0           my $url = $uri->as_string();
156              
157 0           my $res = $self->{ua}->get($url);
158              
159 0 0         if($res->is_error()) {
160 0           Carp::carp("$url API returned error: " . $res->status_line());
161 0           return;
162             }
163              
164 0           my $json = JSON::MaybeXS->new->utf8();
165 0           return $json->decode($res->decoded_content());
166              
167             # my @results = @{ $data || [] };
168             # wantarray ? @results : $results[0];
169             }
170              
171             =head2 ua
172              
173             Accessor method to get and set UserAgent object used internally. You
174             can call I for example, to get the proxy information from
175             environment variables:
176              
177             $geo_coder->ua()->env_proxy(1);
178              
179             You can also set your own User-Agent object:
180              
181             $geo_coder->ua(LWP::UserAgent::Throttled->new());
182              
183             =cut
184              
185             sub ua {
186 0     0 1   my $self = shift;
187 0 0         if (@_) {
188 0           $self->{ua} = shift;
189             }
190 0           $self->{ua};
191             }
192              
193             =head2 reverse_geocode
194              
195             # $location = $geo_coder->reverse_geocode(latlng => '37.778907,-122.39732');
196              
197             # Similar to geocode except it expects a latitude/longitude parameter.
198              
199             Not supported.
200             Croaks if this method is called.
201              
202             =cut
203              
204             sub reverse_geocode {
205             # my $self = shift;
206              
207             # my %param;
208             # if (@_ % 2 == 0) {
209             # %param = @_;
210             # } else {
211             # $param{latlng} = shift;
212             # }
213              
214             # my $latlng = $param{latlng}
215             # or Carp::croak("Usage: reverse_geocode(latlng => \$latlng)");
216              
217             # return $self->geocode(location => $latlng, reverse => 1);
218 0     0 1   Carp::croak(__PACKAGE__, ': Reverse geocode is not supported');
219             }
220              
221             =head2 run
222              
223             In addition to being used as a library within other Perl scripts,
224             L can be run directly from the command line.
225             When invoked this way,
226             it accepts an address as input,
227             performs geocoding,
228             and prints the resulting data structure via L.
229              
230             perl Census.pm 1600 Pennsylvania Avenue NW, Washington DC
231              
232             =cut
233              
234             __PACKAGE__->run(@ARGV) unless caller();
235              
236             sub run {
237 0     0 1   require Data::Dumper;
238              
239 0           my $class = shift;
240              
241 0           my $location = join(' ', @_);
242              
243 0           my @rc = $class->new()->geocode($location);
244              
245 0 0         die "$0: geocoding failed" unless(scalar(@rc));
246              
247 0           print Data::Dumper->new([\@rc])->Dump();
248             }
249              
250             =head1 AUTHOR
251              
252             Nigel Horne
253              
254             Based on L.
255              
256             This library is free software; you can redistribute it and/or modify
257             it under the same terms as Perl itself.
258              
259             Lots of thanks to the folks at geocoding.geo.census.gov.
260              
261             =head1 BUGS
262              
263             =head1 SEE ALSO
264              
265             L, L
266              
267             https://www.census.gov/data/developers/data-sets/Geocoding-services.html
268              
269             =head1 LICENSE AND COPYRIGHT
270              
271             Copyright 2017-2025 Nigel Horne.
272              
273             This program is released under the following licence: GPL2
274              
275             =cut
276              
277             1;