• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nigelhorne / Geo-Coder-List / 13426423435

20 Feb 2025 02:18AM UTC coverage: 4.826%. Remained the same
13426423435

push

github

18 of 373 relevant lines covered (4.83%)

0.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

4.83
/lib/Geo/Coder/List.pm
1
package Geo::Coder::List;
2

3
use 5.10.1;
19✔
4

5
use warnings;
19✔
6
use strict;
19✔
7
use Carp;
19✔
8
use HTML::Entities;
19✔
9
use Params::Get 0.04;
19✔
10
use Object::Configure 0.13;
19✔
11
use Time::HiRes;
12
use Scalar::Util;
19✔
13

14
use constant DEBUG => 0;        # Default debugging level
15

16
# TODO: investigate Geo, Coder::ArcGIS
17
# TODO: return a Geo::Location::Point object all the time
18

19
=head1 NAME
20

21
Geo::Coder::List - Call many Geo-Coders
22

23
=head1 VERSION
24

25
Version 0.35
26

27
=cut
28

29
our $VERSION = '0.35';
30

31
=head1 SYNOPSIS
32

33
L<Geo::Coder::All>
34
and
35
L<Geo::Coder::Many>
36
are great routines but neither quite does what I want.
37

38
C<Geo::Coder::List> is designed to simplify geocoding tasks by aggregating multiple geocoding services into a single, unified interface.
39
It allows developers to chain and prioritize various geocoding backends (such as Google Places, OpenStreetMap, and GeoNames)
40
based on specific conditions,
41
such as location or usage limits.
42
The module features built-in caching mechanisms to optimize performance and reduce redundant API calls,
43
while also normalizing responses from different providers into a consistent format for easier integration with mapping systems such as L<HTML::OSM> and <L<HTML::GoogleMaps::V3>.
44

45
=head1 SUBROUTINES/METHODS
46

47
=head2 new
48

49
Creates a C<Geo::Coder::List> object.
50

51
Takes an optional argument C<cache> which is a reference to a HASH or an object that supports C<get()> and C<set()> methods.
52
The licences of some geo coders,
53
such as Google,
54
specifically prohibit caching API calls,
55
so be careful to only use those services that allow it.
56

57
Takes an optional argument C<debug>,
58
the higher the number,
59
the more debugging.
60

61
    use Geo::Coder::List;
62
    use CHI;
63

64
    my $geocoder->new(cache => CHI->new(driver => 'Memory', global => 1));
65

66
The class can be configured at runtime using environments and configuration files,
67
for example,
68
setting C<$ENV{'GEO__CODER__LIST__carp_on_warn'}> causes warnings to use L<Carp>.
69
For more information about configuring object constructors at runtime,
9✔
70
see L<Object::Configure>.
71

72
=cut
9✔
73

9✔
74
sub new
75
{
1✔
76
        my $class = shift;
77
        my $params = Params::Get::get_params(undef, @_) || {};
78

8✔
79
        if(!defined($class)) {
80
                if((scalar keys %{$params}) > 0) {
81
                        # Using Geo::Coder::List::new(), not Geo::Coder::List->new()
×
82
                        carp(__PACKAGE__, ' use ->new() not ::new() to instantiate');
×
83
                        return;
84
                }
85

9✔
86
                # FIXME: this only works when no arguments are given
2✔
87
                $class = __PACKAGE__;
88
        } elsif(Scalar::Util::blessed($class)) {
×
89
                # If $class is an object, clone it with new arguments
×
90
                return bless { %{$class}, %{$params} }, ref($class);
91
        }
92

93
        $params = Object::Configure::configure($class, $params);
2✔
94

95
        # Return the blessed object
96
        # Locations is an L1 cache that is always used
2✔
97
        return bless { debug => DEBUG, locations => {}, geocoders => [], log => [], %{$params} }, $class;
98
}
99

100
=head2 push
7✔
101

102
Add an encoder to the list of encoders.
103

104
    use Geo::Coder::List;
105
    use Geo::Coder::GooglePlaces;
106
    # ...
107
    my $list = Geo::Coder::List->new()->push(Geo::Coder::GooglePlaces->new());
108

109
Different encoders can be preferred for different locations.
110
For example, this code uses geocode.ca for Canada and US addresses,
111
and OpenStreetMap for other places:
112

113
    my $geo_coderlist = Geo::Coder::List->new()
114
        ->push({ regex => qr/(Canada|USA|United States)$/, geocoder => Geo::Coder::CA->new() })
115
        ->push(Geo::Coder::OSM->new());
116

117
    # Uses Geo::Coder::CA, and if that fails, uses Geo::Coder::OSM
118
    my $location = $geo_coderlist->geocode(location => '1600 Pennsylvania Ave NW, Washington DC, USA');
119
    # Only uses Geo::Coder::OSM
120
    if($location = $geo_coderlist->geocode('10 Downing St, London, UK')) {
121
        print 'The prime minister lives at co-ordinates ',
122
            $location->{geometry}{location}{lat}, ',',
123
            $location->{geometry}{location}{lng}, "\n";
124
    }
125

126
    # It is also possible to limit the number of enquires used by a particular encoder
127
    $geo_coderlist->push({ geocoder => Geo::Coder::GooglePlaces->new(key => '1234', limit => 100) });
128

129
=cut
130

131
sub push
132
{
133
        my($self, $geocoder) = @_;        # Don't use Params::Get or else the regex will be lost
134

135
        push @{$self->{geocoders}}, $geocoder;
×
136

137
        return $self;
×
138
}
139

×
140
=head2 geocode
141

142
Runs geocode on all of the loaded drivers.
143
See L<Geo::Coder::GooglePlaces::V3> for an explanation.
144

145
The name of the Geo-Coder that gave the result is put into the geocode element of the
146
return value,
147
if the value was retrieved from the cache the value will be undefined.
148

149
    if(defined($location->{'geocoder'})) {
150
        print 'Location information retrieved using ', $location->{'geocoder'}, "\n";
151
    }
152

153
=cut
154

155
sub geocode {
156
        my $self = shift;
157
        my $params = Params::Get::get_params('location', @_);
158

×
159
        my $location = $params->{'location'};
×
160

161
        if((!defined($location)) || (length($location) == 0)) {
×
162
                Carp::carp(__PACKAGE__, ' usage: geocode(location => $location)');
163
                return;
×
164
        }
×
165

×
166
        # Fail when the input is just a set of numbers
167
        if($params->{'location'} !~ /\D/) {
168
                Carp::croak('Usage: ', __PACKAGE__, ": invalid input to geocode(), ", $params->{location});
169
                return;
×
170
        }
×
171

×
172
        $location =~ s/\s\s+/ /g;
173
        $location = decode_entities($location);
174
        print "location: $location\n" if($self->{'debug'});
×
175

×
176
        my @call_details = caller(0);
×
177
        if((!wantarray) && (my $rc = $self->_cache($location))) {
178
                if(ref($rc) eq 'ARRAY') {
×
179
                        $rc = $rc->[0];
×
180
                }
×
181
                if(ref($rc) eq 'HASH') {
×
182
                        $rc->{'geocoder'} = 'cache';
183
                        my $log = {
×
184
                                line => $call_details[2],
×
185
                                location => $location,
×
186
                                timetaken => 0,
187
                                gecoder => 'cache',
188
                                wantarray => 0,
189
                                result => $rc
190
                        };
191
                        CORE::push @{$self->{'log'}}, $log;
192
                        print __PACKAGE__, ': ', __LINE__,  ": cached\n" if($self->{'debug'});
193
                        return $rc;
×
194
                }
×
195
        }
×
196
        if(defined($self->_cache($location)) && (ref($self->_cache($location)) eq 'ARRAY') && (my @rc = @{$self->_cache($location)})) {
197
                if(scalar(@rc)) {
198
                        my $allempty = 1;
×
199
                        foreach (@rc) {
×
200
                                if(ref($_) eq 'HASH') {
×
201
                                        if(defined($_->{geometry}{location}{lat})) {
×
202
                                                $allempty = 0;
×
203
                                                $_->{'geocoder'} = 'cache';
×
204
                                        } else {
×
205
                                                delete $_->{'geometry'};
×
206
                                        }
207
                                } elsif(ref($_) eq 'Geo::Location::Point') {
×
208
                                        $allempty = 0;
209
                                        $_->{'geocoder'} = 'cache';
210
                                } else {
×
211
                                        print STDERR Data::Dumper->new([\@rc])->Dump();
×
212
                                        Carp::croak(ref($self), " '$location': unexpected item in the cache");
213
                                }
×
214
                        }
×
215
                        my $log = {
216
                                line => $call_details[2],
217
                                location => $location,
×
218
                                timetaken => 0,
219
                                gecoder => 'cache',
220
                                wantarray => wantarray,
221
                                result => \@rc
222
                        };
223
                        CORE::push @{$self->{'log'}}, $log;
224
                        print __PACKAGE__, ': ', __LINE__,  ": cached\n" if($self->{'debug'});
225
                        if($allempty) {
×
226
                                return;
×
227
                        }
×
228
                        return (wantarray) ? @rc : $rc[0];
×
229
                }
230
        }
×
231

232
        # my $error;
233

234
        ENCODER: foreach my $g(@{$self->{geocoders}}) {
235
                my $geocoder = $g;
236
                if(ref($geocoder) eq 'HASH') {
×
237
                        if(exists($geocoder->{'limit'}) && defined(my $limit = $geocoder->{'limit'})) {
×
238
                                print "limit: $limit\n" if($self->{'debug'});
×
239
                                if($limit <= 0) {
×
240
                                        next;
×
241
                                }
×
242
                                $geocoder->{'limit'}--;
×
243
                        }
244
                        if(my $regex = $geocoder->{'regex'}) {
×
245
                                print 'consider ', ref($geocoder->{geocoder}), ": $regex\n" if($self->{'debug'});
246
                                if($location !~ $regex) {
×
247
                                        next;
×
248
                                }
×
249
                        }
×
250
                        $geocoder = $g->{'geocoder'};
251
                }
252
                my @rc;
×
253
                my $timetaken = Time::HiRes::time();
254
                eval {
×
255
                        # e.g. over QUERY LIMIT with this one
×
256
                        # TODO: remove from the list of geocoders
×
257
                        print 'trying ', ref($geocoder), "\n" if($self->{'debug'});
258
                        if(ref($geocoder) eq 'Geo::GeoNames') {
259
                                print 'username => ', $geocoder->username(), "\n" if($self->{'debug'});
×
260
                                die 'lost username' if(!defined($geocoder->username()));
×
261
                                @rc = $geocoder->geocode($location);
×
262
                        } else {
×
263
                                @rc = $geocoder->geocode(%{$params});
×
264
                        }
265
                };
×
266
                if($@) {
267
                        my $log = {
268
                                line => $call_details[2],
×
269
                                location => $location,
×
270
                                geocoder => ref($geocoder),
271
                                timetaken => Time::HiRes::time() - $timetaken,
272
                                wantarray => wantarray,
273
                                error => $@
274
                        };
275
                        CORE::push @{$self->{'log'}}, $log;
276
                        Carp::carp(ref($geocoder), " '$location': $@");
277
                        # $error = $@;
×
278
                        next ENCODER;
×
279
                }
280
                $timetaken = Time::HiRes::time() - $timetaken;
×
281
                if((ref($geocoder) eq 'Geo::Coder::US::Census') &&
282
                   !(defined($rc[0]->{result}{addressMatches}[0]->{coordinates}{y}))) {
×
283
                           # Looks like Geo::Coder::US::Census sometimes says it's worked when it hasn't
×
284
                        my $log = {
285
                                line => $call_details[2],
286
                                location => $location,
×
287
                                timetaken => $timetaken,
288
                                geocoder => 'Geo::Coder::US::Census',
289
                                wantarray => wantarray,
290
                                result => 'not found',
291
                        };
292
                        CORE::push @{$self->{'log'}}, $log;
293
                        next ENCODER;
294
                }
×
295
                if((scalar(@rc) == 0) ||
×
296
                   ((ref($rc[0]) eq 'HASH') && (scalar(keys %{$rc[0]}) == 0)) ||
297
                   ((ref($rc[0]) eq 'ARRAY') && (scalar(keys %{$rc[0][0]}) == 0))) {
×
298
                        my $log = {
×
299
                                line => $call_details[2],
×
300
                                location => $location,
×
301
                                timetaken => $timetaken,
302
                                geocoder => ref($geocoder),
303
                                wantarray => wantarray,
304
                                result => 'not found',
305
                        };
306
                        CORE::push @{$self->{'log'}}, $log;
307
                        next ENCODER;
308
                }
×
309
                POSSIBLE_LOCATION: foreach my $l(@rc) {
×
310
                        if(ref($l) eq 'ARRAY') {
311
                                # Geo::GeoNames
×
312
                                # FIXME: should consider all locations in the array
×
313
                                $l = $l->[0];
314
                        }
315
                        if((!defined($l)) || ($l eq '')) {
×
316
                                my $log = {
317
                                        line => $call_details[2],
×
318
                                        location => $location,
×
319
                                        timetaken => $timetaken,
320
                                        geocoder => ref($geocoder),
321
                                        wantarray => wantarray,
322
                                        result => 'not found',
323
                                };
324
                                CORE::push @{$self->{'log'}}, $log;
325
                                next ENCODER;
326
                        }
×
327
                        $l->{'geocoder'} = ref($geocoder);
×
328
                        print ref($geocoder), ': ',
329
                                Data::Dumper->new([\$l])->Dump() if($self->{'debug'} >= 2);
×
330
                        last if(ref($l) eq 'Geo::Location::Point');
331
                        next if(ref($l) ne 'HASH');
×
332
                        if($l->{'error'}) {
×
333
                                my $log = {
×
334
                                        line => $call_details[2],
×
335
                                        location => $location,
336
                                        timetaken => $timetaken,
337
                                        geocoder => ref($geocoder),
338
                                        wantarray => wantarray,
339
                                        error => $l->{'error'}
340
                                };
341
                                CORE::push @{$self->{'log'}}, $log;
×
342
                                next ENCODER;
343
                        } else {
×
344
                                # Try to create a common interface, helps with HTML::GoogleMaps::V3
×
345
                                if(!defined($l->{geometry}{location}{lat})) {
346
                                        my ($lat, $long);
347
                                        if($l->{lat} && defined($l->{lon})) {
×
348
                                                # OSM/RandMcNalley
×
349
                                                # This would have been nice, but it doesn't compile
×
350
                                                # ($lat, $long) = $l->{'lat', 'lon'};
351
                                                $lat = $l->{lat};
352
                                                $long = $l->{lon};
353
                                                $l->{'debug'} = __LINE__;
×
354
                                        } elsif($l->{BestLocation}) {
×
355
                                                # Bing
×
356
                                                $lat = $l->{BestLocation}->{Coordinates}->{Latitude};
357
                                                $long = $l->{BestLocation}->{Coordinates}->{Longitude};
358
                                                $l->{'debug'} = __LINE__;
×
359
                                        } elsif($l->{point}) {
×
360
                                                # Bing
×
361
                                                $lat = $l->{point}->{coordinates}[0];
362
                                                $long = $l->{point}->{coordinates}[1];
363
                                                $l->{'debug'} = __LINE__;
×
364
                                        } elsif($l->{latt}) {
×
365
                                                # geocoder.ca
×
366
                                                $lat = $l->{latt};
367
                                                $long = $l->{longt};
368
                                                $l->{'debug'} = __LINE__;
×
369
                                        } elsif($l->{latitude}) {
×
370
                                                # postcodes.io
×
371
                                                # Geo::Coder::Free
372
                                                $lat = $l->{latitude};
373
                                                $long = $l->{longitude};
374
                                                if(my $type = $l->{'local_type'}) {
×
375
                                                        $l->{'type'} = lcfirst($type);        # e.g. village
×
376
                                                }
×
377
                                                $l->{'debug'} = __LINE__;
×
378
                                        } elsif($l->{'properties'}{'geoLatitude'}) {
379
                                                # ovi
×
380
                                                $lat = $l->{properties}{geoLatitude};
381
                                                $long = $l->{properties}{geoLongitude};
382
                                                $l->{'debug'} = __LINE__;
×
383
                                        } elsif($l->{'results'}[0]->{'geometry'}) {
×
384
                                                if($l->{'results'}[0]->{'geometry'}->{'location'}) {
×
385
                                                        # DataScienceToolkit
386
                                                        $lat = $l->{'results'}[0]->{'geometry'}->{'location'}->{'lat'};
×
387
                                                        $long = $l->{'results'}[0]->{'geometry'}->{'location'}->{'lng'};
388
                                                        $l->{'debug'} = __LINE__;
×
389
                                                } else {
×
390
                                                        # OpenCage
×
391
                                                        $lat = $l->{'results'}[0]->{'geometry'}->{'lat'};
392
                                                        $long = $l->{'results'}[0]->{'geometry'}->{'lng'};
393
                                                        $l->{'debug'} = __LINE__;
×
394
                                                }
×
395
                                        } elsif($l->{'RESULTS'}) {
×
396
                                                # GeoCodeFarm
397
                                                $lat = $l->{'RESULTS'}[0]{'COORDINATES'}{'latitude'};
398
                                                $long = $l->{'RESULTS'}[0]{'COORDINATES'}{'longitude'};
399
                                                $l->{'debug'} = __LINE__;
×
400
                                        } elsif(defined($l->{result}{addressMatches}[0]->{coordinates}{y})) {
×
401
                                                # US Census
×
402
                                                # This would have been nice, but it doesn't compile
403
                                                # ($lat, $long) = $l->{result}{addressMatches}[0]->{coordinates}{y, x};
404
                                                $lat = $l->{result}{addressMatches}[0]->{coordinates}{y};
405
                                                $long = $l->{result}{addressMatches}[0]->{coordinates}{x};
406
                                                $l->{'debug'} = __LINE__;
×
407
                                        } elsif($l->{lat}) {
×
408
                                                # Geo::GeoNames
×
409
                                                $lat = $l->{lat};
410
                                                $long = $l->{lng};
411
                                                $l->{'debug'} = __LINE__;
×
412
                                        } elsif($l->{features}) {
×
413
                                                if($l->{features}[0]->{center}) {
×
414
                                                        # Geo::Coder::Mapbox
415
                                                        $lat = $l->{features}[0]->{center}[1];
×
416
                                                        $long = $l->{features}[0]->{center}[0];
417
                                                        $l->{'debug'} = __LINE__;
×
418
                                                } elsif($l->{'features'}[0]{'geometry'}{'coordinates'}) {
×
419
                                                        # Geo::Coder::GeoApify
×
420
                                                        $lat = $l->{'features'}[0]{'geometry'}{'coordinates'}[1];
421
                                                        $long = $l->{'features'}[0]{'geometry'}{'coordinates'}[0];
422
                                                        $l->{'debug'} = __LINE__;
×
423
                                                } else {
×
424
                                                        # GeoApify doesn't give an error if a location is not found
×
425
                                                        next ENCODER;
426
                                                }
427
                                        } else {
×
428
                                                $l->{'debug'} = __LINE__;
429
                                        }
430

×
431
                                        if(defined($lat) && defined($long)) {
432
                                                $l->{geometry}{location}{lat} = $lat;
433
                                                $l->{geometry}{location}{lng} = $long;
×
434
                                                # Compatibility
×
435
                                                $l->{'lat'} = $lat;
×
436
                                                $l->{'lon'} = $long;
437
                                        } else {
×
438
                                                delete $l->{'geometry'};
×
439
                                                delete $l->{'lat'};
440
                                                delete $l->{'lon'};
×
441
                                        }
×
442

×
443
                                        if($l->{'standard'}{'countryname'}) {
444
                                                # geocoder.xyz
445
                                                $l->{'address'}{'country'} = $l->{'standard'}{'countryname'};
×
446
                                        }
447
                                }
×
448
                                if(defined($l->{geometry}{location}{lat})) {
449
                                        print $l->{geometry}{location}{lat}, '/', $l->{geometry}{location}{lng}, "\n" if($self->{'debug'});
450
                                        $l->{geocoder} = $geocoder;
×
451
                                        $l->{'lat'} //= $l->{geometry}{location}{lat};
×
452
                                        $l->{'lng'} //= $l->{geometry}{location}{lng};
×
453
                                        $l->{'lon'} //= $l->{geometry}{location}{lng};
×
454
                                        my $log = {
×
455
                                                line => $call_details[2],
×
456
                                                location => $location,
×
457
                                                timetaken => $timetaken,
458
                                                geocoder => ref($geocoder),
459
                                                wantarray => wantarray,
460
                                                result => $l
461
                                        };
462
                                        CORE::push @{$self->{'log'}}, $log;
463
                                        last POSSIBLE_LOCATION;
464
                                }
×
465
                        }
×
466
                }
467

×
468
                if(scalar(@rc)) {
469
                        print 'Number of matches from ', ref($geocoder), ': ', scalar(@rc), "\n" if($self->{'debug'});
470
                        $Data::Dumper::Maxdepth = 10;
471
                        print Data::Dumper->new([\@rc])->Dump() if($self->{'debug'} >= 2);
×
472
                        if(defined($rc[0])) {        # check it's not an empty hash
×
473
                                if(defined($rc[0]->{'long'}) && !defined($rc[0]->{'lng'})) {
×
474
                                        $rc[0]->{'lng'} = $rc[0]->{'long'};
×
475
                                }
×
476
                                if(defined($rc[0]->{'long'}) && !defined($rc[0]->{'lon'})) {
×
477
                                        $rc[0]->{'lon'} = $rc[0]->{'long'};
×
478
                                }
479
                                if((!defined($rc[0]->{lat})) || (!defined($rc[0]->{lng}))) {
×
480
                                        # ::diag(Data::Dumper->new([\@rc])->Dump());
481
                                        warn Data::Dumper->new([\@rc])->Dump();
×
482
                                        Carp::croak("BUG: '$location': HASH exists but is not sensible");
×
483
                                }
484
                                if(wantarray) {
×
485
                                        $self->_cache($location, \@rc);
×
486
                                        return @rc;
×
487
                                }
488
                                $self->_cache($location, $rc[0]);
×
489
                                return $rc[0];
×
490
                        }
491
                }
492
        }
493
        # Can't do this because we need to return undef in this case
494
        # if($error) {
495
                # return { error => $error };
496
        # }
497
        print "No matches\n" if($self->{'debug'});
×
498
        if(wantarray) {
×
499
                $self->_cache($location, ());
×
500
                return ();
×
501
        }
502
        $self->_cache($location, undef);
×
503
}
504

505
=head2 ua
506

507
Accessor method to set the UserAgent object used internally by each of the Geo-Coders.
508
You can call I<env_proxy>,
509
for example,
510
to set the proxy information from environment variables:
511

512
    my $geocoder_list = Geo::Coder::List->new();
513
    my $ua = LWP::UserAgent->new();
514
    $ua->env_proxy(1);
515
    $geocoder_list->ua($ua);
516

517
Note that unlike Geo::Coders,
518
there is no read method since that would be pointless.
519

520
=cut
521

522
sub ua
523
{
524
        my($self, $ua) = @_;
×
525
        return unless $ua;
×
526

527
        foreach my $g(@{$self->{geocoders}}) {
×
528
                my $geocoder = (ref($g) eq 'HASH') ? $g->{geocoder} : $g;
×
529
                Carp::croak('No geocoder found') unless defined $geocoder;
×
530
                $geocoder->ua($ua);
×
531
        }
532

533
        return $ua;
×
534
}
535

536
=head2 reverse_geocode
537

538
Similar to geocode except it expects a latitude/longitude parameter.
539

540
    print $geocoder_list->reverse_geocode(latlng => '37.778907,-122.39732');
541

542
=cut
543

544
sub reverse_geocode {
545
        my $self = shift;
×
546
        my $params = Params::Get::get_params('latlng', @_);
×
547

548
        my $latlng = $params->{'latlng'}
×
549
                or Carp::croak('Usage: reverse_geocode(latlng => $location)');
550

551
        my ($latitude, $longitude);
×
552
        if($latlng) {
×
553
                ($latitude, $longitude) = split(/,/, $latlng);
×
554
                $params->{'lat'} //= $latitude;
×
555
                $params->{'lon'} //= $longitude;
×
556
        } else {
557
                $latitude //= $params->{'lat'};
×
558
                $longitude //= $params->{'lon'};
×
559
                $longitude //= $params->{'long'};
×
560
                $latlng = $params->{'latlng'} = "$latitude,$longitude";
×
561
        }
562

563
        if(my $rc = $self->_cache($latlng)) {
×
564
                return $rc;
×
565
        }
566

567
        foreach my $g(@{$self->{geocoders}}) {
×
568
                my $geocoder = $g;
×
569
                if(ref($geocoder) eq 'HASH') {
×
570
                        if(exists($geocoder->{'limit'}) && defined(my $limit = $geocoder->{'limit'})) {
×
571
                                print "limit: $limit\n" if($self->{'debug'});
×
572
                                if($limit <= 0) {
×
573
                                        next;
×
574
                                }
575
                                $geocoder->{'limit'}--;
×
576
                        }
577
                        $geocoder = $g->{'geocoder'};
×
578
                }
579
                print 'trying ', ref($geocoder), "\n" if($self->{'debug'});
×
580
                if(wantarray) {
×
581
                        my @rc;
×
582
                        if(my @locs = $geocoder->reverse_geocode(%{$params})) {
×
583
                                print Data::Dumper->new([\@locs])->Dump() if($self->{'debug'} >= 2);
×
584
                                foreach my $loc(@locs) {
×
585
                                        if(my $name = $loc->{'display_name'}) {
×
586
                                                # OSM
587
                                                CORE::push @rc, $name;
×
588
                                        } elsif($loc->{'city'}) {
589
                                                # Geo::Coder::CA
590
                                                my $name;
×
591
                                                if(my $usa = $loc->{'usa'}) {
×
592
                                                        $name = $usa->{'usstnumber'};
×
593
                                                        if(my $staddress = $usa->{'usstaddress'}) {
×
594
                                                                $name .= ' ' if($name);
×
595
                                                                $name .= $staddress;
×
596
                                                        }
597
                                                        if(my $city = $usa->{'uscity'}) {
×
598
                                                                $name .= ', ' if($name);
×
599
                                                                $name .= $city;
×
600
                                                        }
601
                                                        if(my $state = $usa->{'state'}) {
×
602
                                                                $name .= ', ' if($name);
×
603
                                                                $name .= $state;
×
604
                                                        }
605
                                                        $name .= ', ' if($name);
×
606
                                                        $name .= 'USA';
×
607
                                                } else {
608
                                                        $name = $loc->{'stnumber'};
×
609
                                                        if(my $staddress = $loc->{'staddress'}) {
×
610
                                                                $name .= ' ' if($name);
×
611
                                                                $name .= $staddress;
×
612
                                                        }
613
                                                        if(my $city = $loc->{'city'}) {
×
614
                                                                $name .= ', ' if($name);
×
615
                                                                $name .= $city;
×
616
                                                        }
617
                                                        if(my $state = $loc->{'prov'}) {
×
618
                                                                $state .= ', ' if($name);
×
619
                                                                $name .= $state;
×
620
                                                        }
621
                                                }
622
                                                CORE::push @rc, $name;
×
623
                                        } elsif($loc->{features}) {
624
                                                # Geo::Coder::Apify
625
                                                return CORE::push @rc, $loc->{features}[0]->{properties}{formatted};
×
626
                                        }
627
                                }
628
                        }
629
                        $self->_cache($latlng, \@rc);
×
630
                        return @rc;
×
631
                } elsif(my $rc = $self->_cache($latlng) // $geocoder->reverse_geocode(%{$params})) {
×
632
                        return $rc if(!ref($rc));
×
633
                        print Data::Dumper->new([$rc])->Dump() if($self->{'debug'} >= 2);
×
634
                        if(my $name = $rc->{'display_name'}) {
×
635
                                # OSM
636
                                return $self->_cache($latlng, $name);
×
637
                        }
638
                        if($rc->{'city'}) {
×
639
                                # Geo::Coder::CA
640
                                my $name;
×
641
                                if(my $usa = $rc->{'usa'}) {
×
642
                                        # TODO: Use Lingua::Conjunction
643
                                        $name = $usa->{'usstnumber'};
×
644
                                        if(my $staddress = $usa->{'usstaddress'}) {
×
645
                                                $name .= ' ' if($name);
×
646
                                                $name .= $staddress;
×
647
                                        }
648
                                        if(my $city = $usa->{'uscity'}) {
×
649
                                                $name .= ', ' if($name);
×
650
                                                $name .= $city;
×
651
                                        }
652
                                        if(my $state = $usa->{'state'}) {
×
653
                                                $name .= ', ' if($name);
×
654
                                                $name .= $state;
×
655
                                        }
656
                                        return $self->_cache($latlng, "$name, USA");
×
657
                                } else {
658
                                        # TODO: Use Lingua::Conjunction
659
                                        $name = $rc->{'stnumber'};
×
660
                                        if(my $staddress = $rc->{'staddress'}) {
×
661
                                                $name .= ' ' if($name);
×
662
                                                $name .= $staddress;
×
663
                                        }
664
                                        if(my $city = $rc->{'city'}) {
×
665
                                                $name .= ', ' if($name);
×
666
                                                $name .= $city;
×
667
                                        }
668
                                        if(my $state = $rc->{'prov'}) {
×
669
                                                $state = ", $state" if($name);
×
670
                                                return $self->_cache($latlng, "$name $state");
×
671
                                        }
672
                                }
673
                                return $self->_cache($latlng, $name);
×
674
                        }
675
                        if($rc->{features}) {
×
676
                                # Geo::Coder::Apify
677
                                return $self->_cache($latlng, $rc->{features}[0]->{properties}{formatted});
×
678
                        }
679
                }
680
        }
681
        return;
×
682
}
683

684
=head2 log
685

686
Returns the log of events to help you debug failures,
687
optimize lookup order and fix quota breakage.
688

689
    my @log = @{$geocoderlist->log()};
690

691
=cut
692

693
sub log {
694
        my $self = shift;
×
695

696
        return $self->{'log'};
×
697
}
698

699
=head2 flush
700

701
Clear the log.
702

703
=cut
704

705
sub flush {
706
        my $self = shift;
×
707

708
        delete $self->{'log'};
×
709
}
710

711
sub _cache {
712
        my $self = shift;
×
713
        my $key = shift;
×
714

715
        if(my $value = shift) {
×
716
                # Put something into the cache
717
                $self->{locations}->{$key} = $value;
×
718
                my $rc = $value;
×
719
                if($self->{'cache'}) {
×
720
                        my $duration;
×
721
                        if(ref($value) eq 'ARRAY') {
×
722
                                foreach my $item(@{$value}) {
×
723
                                        if(ref($item) eq 'HASH') {
×
724
                                                $item->{'geocoder'} = ref($item->{'geocoder'});        # It's an object, not the name
×
725
                                                if(!$self->{'debug'}) {
×
726
                                                        while(my($k, $v) = each %{$item}) {
×
727
                                                                delete $item->{$k} unless($k eq 'geometry');
×
728
                                                        }
729
                                                }
730
                                                if(!defined($item->{geometry}{location}{lat})) {
×
731
                                                        if(defined($item->{geometry})) {
×
732
                                                                # Maybe a temporary lookup failure,
733
                                                                # so do a research tomorrow
734
                                                                $duration = '1 day';
×
735
                                                        } else {
736
                                                                # Probably the place doesn't exist
737
                                                                $duration = '1 week';
×
738
                                                        }
739
                                                        $rc = undef;
×
740
                                                }
741
                                        }
742
                                }
743
                                if(!defined($duration)) {
×
744
                                        # Has matched - it won't move
745
                                        $duration = '1 month';
×
746
                                }
747
                        } elsif(ref($value) eq 'HASH') {
748
                                $value->{'geocoder'} = ref($value->{'geocoder'});        # It's an object, not the name
×
749
                                if(!$self->{'debug'}) {
×
750
                                        while(my($k, $v) = each %{$value}) {
×
751
                                                delete $value->{$k} unless ($k eq 'geometry');
×
752
                                        }
753
                                }
754
                                if(defined($value->{geometry}{location}{lat})) {
×
755
                                        $duration = '1 month';        # It won't move :-)
×
756
                                } elsif(defined($value->{geometry})) {
757
                                        # Maybe a temporary lookup failure, so do a research
758
                                        # tomorrow
759
                                        $duration = '1 day';
×
760
                                        $rc = undef;
×
761
                                } else {
762
                                        # Probably the place doesn't exist
763
                                        $duration = '1 week';
×
764
                                        $rc = undef;
×
765
                                }
766
                        } else {
767
                                $duration = '1 month';
×
768
                        }
769
                        print Data::Dumper->new([$value])->Dump() if($self->{'debug'});
×
770
                        if(ref($self->{'cache'}) eq 'HASH') {
×
771
                                $self->{'cache'}->{$key} = $value;
×
772
                        } elsif(!ref($value)) {
773
                                $self->{'cache'}->set($key, $value, $duration);
×
774
                        }
775
                }
776
                return $rc;
×
777
        }
778

779
        # Retrieve from the cache
780
        my $rc = $self->{'locations'}->{$key};        # In the L1 cache?
×
781
        if((!defined($rc)) && $self->{'cache'}) {        # In the L2 cache?
×
782
                if(ref($self->{'cache'}) eq 'HASH') {
×
783
                        $rc = $self->{'cache'}->{$key};
×
784
                } else {
785
                        $rc = $self->{'cache'}->get($key);
×
786
                }
787
        }
788
        if(defined($rc)) {
×
789
                if(ref($rc) eq 'HASH') {        # else - it will be an array of hashes
×
790
                        if(!defined($rc->{geometry}{location}{lat})) {
×
791
                                return;
×
792
                        }
793
                        $rc->{'lat'} //= $rc->{geometry}{location}{lat};
×
794
                        $rc->{'lng'} //= $rc->{geometry}{location}{lng};
×
795
                        $rc->{'lon'} //= $rc->{geometry}{location}{lng};
×
796
                }
797
        }
798
        return $rc;
×
799
}
800

801
=head1 AUTHOR
802

803
Nigel Horne, C<< <njh at bandsman.co.uk> >>
804

805
=head1 BUGS
806

807
Please report any bugs or feature requests to C<bug-geo-coder-list at rt.cpan.org>,
808
or through the web interface at
×
809
L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Geo-Coder-List>.
×
810
I will be notified, and then you'll
811
automatically be notified of progress on your bug as I make changes.
812

×
813
reverse_geocode() doesn't update the logger.
814
reverse_geocode() should support L<Geo::Location::Point> objects.
×
815

×
816
=head1 SEE ALSO
817

818
=over 4
×
819

820
=item * L<Geo::Coder::All>
×
821

822
=item * L<Geo::Coder::GooglePlaces>
×
823

824
=item * L<Geo::Coder::Many>
×
825

826
=item * L<Object::Configure>
×
827

828
=back
×
829

830
=cut
×
831

832
=head1 SUPPORT
833

×
834
This module is provided as-is without any warranty.
835

836
You can find documentation for this module with the perldoc command.
837

838
    perldoc Geo::Coder::List
839

840
You can also look for information at:
841

842
=over 4
843

844
=item * RT: CPAN's request tracker
845

846
L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Geo-Coder-List>
847

848
=item * MetaCPAN
849

850
L<https://metacpan.org/release/Geo-Coder-List>
851

852
=back
853

854
=head1 LICENSE AND COPYRIGHT
855

856
Copyright 2016-2025 Nigel Horne.
857

858
This program is released under the following licence: GPL2
859

860
=cut
861

862
1;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc