File Coverage

lib/Geo/Coder/Free.pm
Criterion Covered Total %
statement 84 165 50.9
branch 25 82 30.4
condition 5 18 27.7
subroutine 11 13 84.6
pod 4 4 100.0
total 129 282 45.7


line stmt bran cond sub pod time code
1             package Geo::Coder::Free;
2              
3 3     3   254608 use strict;
  3         21  
  3         85  
4 3     3   14 use warnings;
  3         5  
  3         72  
5              
6 3     3   722 use Geo::Coder::Free::DB::admin1;
  3         9  
  3         84  
7 3     3   864 use Geo::Coder::Free::DB::admin2;
  3         8  
  3         88  
8 3     3   739 use Geo::Coder::Free::DB::cities;
  3         8  
  3         83  
9 3     3   1207 use Module::Info;
  3         14748  
  3         75  
10 3     3   18 use Carp;
  3         3  
  3         154  
11 3     3   726 use Error::Simple;
  3         11307  
  3         19  
12 3     3   158 use File::Spec;
  3         4  
  3         4068  
13              
14             =head1 NAME
15              
16             Geo::Coder::Free - Provides a geocoding functionality using free databases of towns
17              
18             =head1 VERSION
19              
20             Version 0.03
21              
22             =cut
23              
24             our $VERSION = '0.03';
25              
26             =head1 SYNOPSIS
27              
28             use Geo::Coder::Free;
29              
30             my $geocoder = Geo::Coder::Free->new();
31             my $location = $geocoder->geocode(location => 'Ramsgate, Kent, UK');
32              
33             =head1 DESCRIPTION
34              
35             Geo::Coder::Free provides an interface to free databases.
36              
37             Refer to the source URL for licencing information for these files
38             cities.csv is from https://www.maxmind.com/en/free-world-cities-database
39             admin1.db is from http://download.geonames.org/export/dump/admin1CodesASCII.txt
40             admin2.db is from http://download.geonames.org/export/dump/admin2Codes.txt
41              
42             See also http://download.geonames.org/export/dump/allCountries.zip
43              
44             To significantly speed this up, gunzip cities.csv and run it through the db2sql script to create an SQLite file.
45              
46             =head1 METHODS
47              
48             =head2 new
49              
50             $geocoder = Geo::Coder::Free->new();
51              
52             =cut
53              
54             sub new {
55 3     3 1 715 my($proto, %param) = @_;
56 3   66     17 my $class = ref($proto) || $proto;
57              
58             # Geo::Coder::Free->new not Geo::Coder::Free::new
59 3 100       12 return unless($class);
60              
61             # Geo::Coder::Free::DB::init(directory => 'lib/Geo/Coder/Free/databases');
62              
63 2         17 my $directory = Module::Info->new_from_loaded(__PACKAGE__)->file();
64 2         234 $directory =~ s/\.pm$//;
65 2         77 Geo::Coder::Free::DB::init(directory => File::Spec->catfile($directory, 'databases'));
66              
67 2         10 return bless { }, $class;
68             }
69              
70             =head2 geocode
71              
72             $location = $geocoder->geocode(location => $location);
73              
74             print 'Latitude: ', $location->{'latt'}, "\n";
75             print 'Longitude: ', $location->{'longt'}, "\n";
76              
77             # TODO:
78             # @locations = $geocoder->geocode('Portland, USA');
79             # diag 'There are Portlands in ', join (', ', map { $_->{'state'} } @locations);
80            
81             =cut
82              
83             sub geocode {
84 1     1 1 408 my $self = shift;
85              
86 1         3 my %param;
87 1 50       8 if(ref($_[0]) eq 'HASH') {
    50          
88 0         0 %param = %{$_[0]};
  0         0  
89             } elsif(@_ % 2 == 0) {
90 0         0 %param = @_;
91             } else {
92 1         5 $param{location} = shift;
93             }
94              
95             my $location = $param{location}
96 1 50       8 or Carp::croak("Usage: geocode(location => \$location)");
97              
98 1         19 my $county;
99             my $state;
100 1         0 my $country;
101 1         0 my $country_code;
102 1         0 my $concatenated_codes;
103              
104 1 50       9 if($location =~ /^([\w\s\-]+)?,([\w\s]+),([\w\s]+)?$/) {
    0          
105             # Turn 'Ramsgate, Kent, UK' into 'Ramsgate'
106 1         3 $location = $1;
107 1         3 $county = $2;
108 1         3 $country = $3;
109 1         2 $location =~ s/\-/ /g;
110 1         4 $county =~ s/^\s//g;
111 1         3 $county =~ s/\s$//g;
112 1         3 $country =~ s/^\s//g;
113 1         2 $country =~ s/\s$//g;
114 1 50       5 if($location =~ /^St\.? (.+)/) {
115 0         0 $location = "Saint $1";
116             }
117 1 50 33     6 if(($country eq 'UK') || ($country eq 'United Kingdom')) {
118 0         0 $country = 'Great Britain';
119 0         0 $concatenated_codes = 'GB';
120             }
121             } elsif($location =~ /^([\w\s\-]+)?,([\w\s]+),([\w\s]+),\s*(Canada|United States|USA|US)?$/) {
122 0         0 $location = $1;
123 0         0 $county = $2;
124 0         0 $state = $3;
125 0         0 $country = $4;
126 0         0 $county =~ s/^\s//g;
127 0         0 $county =~ s/\s$//g;
128 0         0 $state =~ s/^\s//g;
129 0         0 $state =~ s/\s$//g;
130 0         0 $country =~ s/^\s//g;
131 0         0 $country =~ s/\s$//g;
132             } else {
133 0         0 Carp::croak(__PACKAGE__, ' only supports towns, not full addresses');
134 0         0 return;
135             }
136              
137 1 50       3 if($country) {
138 1 50       5 if(!defined($self->{'admin1'})) {
139 1 50       13 $self->{'admin1'} = Geo::Coder::Free::DB::admin1->new() or die "Can't open the admin1 database";
140             }
141 1 50       8 if(my $admin1 = $self->{'admin1'}->fetchrow_hashref(asciiname => $country)) {
142 1         113 $concatenated_codes = $admin1->{'concatenated_codes'};
143             } else {
144 0         0 require Locale::Country;
145 0 0       0 if($state) {
146 0 0       0 if($state =~ /^[A-Z]{2}$/) {
147 0         0 $concatenated_codes = uc(Locale::Country::country2code($country)) . ".$state";
148             } else {
149 0         0 $concatenated_codes = uc(Locale::Country::country2code($country));
150 0         0 $country_code = $concatenated_codes;
151 0 0       0 if($state) {
152 0         0 my @admin1s = @{$self->{'admin1'}->selectall_hashref(asciiname => $state)};
  0         0  
153 0         0 foreach my $admin1(@admin1s) {
154 0 0       0 if($admin1->{'concatenated_codes'} =~ /^$concatenated_codes\./i) {
155 0         0 $concatenated_codes = $admin1->{'concatenated_codes'};
156 0         0 last;
157             }
158             }
159             }
160             }
161             } else {
162 0         0 $concatenated_codes = uc(Locale::Country::country2code($country));
163             }
164             }
165             }
166 1 50       7 return unless(defined($concatenated_codes));
167              
168 1 50       118 if(!defined($self->{'admin2'})) {
169 1 50       13 $self->{'admin2'} = Geo::Coder::Free::DB::admin2->new() or die "Can't open the admin1 database";
170             }
171 1         18 my @admin2s;
172             my $region;
173 1         0 my @regions;
174 1 50       6 if($county =~ /^[A-Z]{2}/) {
175             # Canadian province or US state
176 0         0 $region = $county;
177             } else {
178 1         2 @admin2s = @{$self->{'admin2'}->selectall_hashref(asciiname => $county)};
  1         8  
179 1         102 foreach my $admin2(@admin2s) {
180 2 50       31 if($admin2->{'concatenated_codes'} =~ $concatenated_codes) {
181 2         5 $region = $admin2->{'concatenated_codes'};
182 2 50       9 if($region =~ /^[A-Z]{2}\.([A-Z]{2})\./) {
183 0         0 my $rc = $1;
184 0 0       0 if($state =~ /^[A-Z]{2}$/) {
185 0 0       0 if($state eq $rc) {
186 0         0 $region = $rc;
187 0         0 last;
188             }
189             } else {
190 0         0 push @regions, $region;
191 0         0 push @regions, $rc;
192             }
193             } else {
194 2         6 push @regions, $region;
195             }
196             }
197             }
198 1 50 33     5 if($state && !defined($region)) {
199 0 0       0 if($state =~ /^[A-Z]{2}$/) {
200 0         0 $region = $state;
201             } else {
202 0         0 @admin2s = @{$self->{'admin2'}->selectall_hashref(asciiname => $state)};
  0         0  
203 0         0 foreach my $admin2(@admin2s) {
204 0 0       0 if($admin2->{'concatenated_codes'} =~ $concatenated_codes) {
205 0         0 $region = $admin2->{'concatenated_codes'};
206 0         0 last;
207             }
208             }
209             }
210             }
211             }
212              
213 1 50 33     5 if((scalar(@regions) == 0) && (!defined($region))) {
214             # e.g. Unitary authorities in the UK
215 0         0 @admin2s = @{$self->{'admin2'}->selectall_hashref(asciiname => $location)};
  0         0  
216 0 0 0     0 if(scalar(@admin2s) && defined($admin2s[0]->{'concatenated_codes'})) {
217 0         0 foreach my $admin2(@admin2s) {
218 0 0       0 if($admin2->{'concatenated_codes'} =~ $concatenated_codes) {
219 0         0 $region = $admin2->{'concatenated_codes'};
220 0         0 last;
221             }
222             }
223             } else {
224             # e.g. states in the US
225 0         0 my @admin1s = @{$self->{'admin1'}->selectall_hashref(asciiname => $county)};
  0         0  
226 0         0 foreach my $admin1(@admin1s) {
227 0 0       0 if($admin1->{'concatenated_codes'} =~ /^$concatenated_codes\./i) {
228 0         0 $region = $admin1->{'concatenated_codes'};
229 0         0 last;
230             }
231             }
232             }
233             }
234              
235 1 50       5 if(!defined($self->{'cities'})) {
236 1         17 $self->{'cities'} = Geo::Coder::Free::DB::cities->new();
237             }
238              
239 1         4 my $options = { City => lc($location) };
240 1 50       4 if($region) {
241 1 50       6 if($region =~ /^.+\.(.+)$/) {
242 1         2 $region = $1;
243             }
244 1         3 $options->{'Region'} = $region;
245 1 50       4 if($country_code) {
246 0         0 $options->{'Country'} = lc($country_code);
247             }
248             }
249              
250             # This case nonsense is because DBD::CSV changes the columns to lowercase, wherease DBD::SQLite does not
251 1 50       4 if(wantarray) {
252 0         0 my @rc = @{$self->{'cities'}->selectall_hashref($options)};
  0         0  
253 0         0 foreach my $city(@rc) {
254 0 0       0 if($city->{'Latitude'}) {
255 0         0 $city->{'latitude'} = delete $city->{'Latitude'};
256 0         0 $city->{'longitude'} = delete $city->{'Longitude'};
257             }
258             }
259 0         0 return @rc;
260             }
261 1         6 my $city = $self->{'cities'}->fetchrow_hashref($options);
262 0 0         if(!defined($city)) {
263 0           foreach $region(@regions) {
264 0 0         if($region =~ /^.+\.(.+)$/) {
265 0           $region = $1;
266             }
267 0           $options->{'Region'} = $region;
268 0           $city = $self->{'cities'}->fetchrow_hashref($options);
269 0 0         last if(defined($city));
270             }
271             }
272              
273 0 0 0       if(defined($city) && $city->{'Latitude'}) {
274 0           $city->{'latitude'} = delete $city->{'Latitude'};
275 0           $city->{'longitude'} = delete $city->{'Longitude'};
276             }
277 0           return $city;
278             # my $rc;
279             # if(wantarray && $rc->{'otherlocations'} && $rc->{'otherlocations'}->{'loc'} &&
280             # (ref($rc->{'otherlocations'}->{'loc'}) eq 'ARRAY')) {
281             # my @rc = @{$rc->{'otherlocations'}->{'loc'}};
282             # if(scalar(@rc)) {
283             # return @rc;
284             # }
285             # }
286             # return $rc;
287             # my @results = @{ $data || [] };
288             # wantarray ? @results : $results[0];
289             }
290              
291             =head2 reverse_geocode
292              
293             $location = $geocoder->reverse_geocode(latlng => '37.778907,-122.39732');
294              
295             To be done.
296              
297             =cut
298              
299             sub reverse_geocode {
300 0     0 1   Carp::croak('Reverse lookup is not yet supported');
301             };
302              
303             =head2 ua
304              
305             Does nothing, here for compatibility with other geocoders
306              
307             =cut
308              
309       0 1   sub ua {
310             };
311              
312             =head1 AUTHOR
313              
314             Nigel Horne
315              
316             This library is free software; you can redistribute it and/or modify
317             it under the same terms as Perl itself.
318              
319             =head1 BUGS
320              
321             Lots of lookups fail at the moment.
322              
323             =head1 SEE ALSO
324              
325             VWF, Maxmind and geonames.
326              
327             =head1 LICENSE AND COPYRIGHT
328              
329             Copyright 2017 Nigel Horne.
330              
331             The program code is released under the following licence: GPL for personal use on a single computer.
332             All other users (including Commercial, Charity, Educational, Government)
333             must apply in writing for a licence for use from Nigel Horne at ``.
334              
335             =cut
336              
337             1;