| File: | blib/lib/Geo/Coder/List.pm |
| Coverage: | 93.5% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package Geo::Coder::List; | |||||
| 2 | ||||||
| 3 | # Geo::Coder::List - Aggregate and chain multiple geocoding backends | |||||
| 4 | ||||||
| 5 | 25 25 | 1832903 34 | use 5.10.1; | |||
| 6 | ||||||
| 7 | 25 25 25 | 46 16 211 | use strict; | |||
| 8 | 25 25 25 | 31 25 377 | use warnings; | |||
| 9 | ||||||
| 10 | 25 25 25 | 38 23 666 | use Carp; | |||
| 11 | 25 25 25 | 296 2913 478 | use Data::Dumper; | |||
| 12 | # One definition of what counts as a location: Geo::Location::Predicate holds | |||||
| 13 | # the rule the facade and its backends apply, so what is refused here is refused | |||||
| 14 | # there for the same reason. It is called by name rather than imported - an | |||||
| 15 | # import copies the code reference into this package, and then a test could not | |||||
| 16 | # swap the rule for a spy to prove that it is the rule being consulted. | |||||
| 17 | # | |||||
| 18 | # The rule is an optional dependency, not a prerequisite: it is a distribution of | |||||
| 19 | # its own (Geo-Location-Predicate), declared in Makefile.PL under META_MERGE's | |||||
| 20 | # recommends, so an installer fetches it by default and a standalone install that | |||||
| 21 | # leaves it out still gets the one answer this module can give alone - a | |||||
| 22 | # reference is refused. It is looked for when it is first needed rather than | |||||
| 23 | # when this module is loaded, so importing this module reads no file it may not | |||||
| 24 | # need, and a rule installed after this module is loaded is still found. | |||||
| 25 | my $HAVE_RULE; # undef until the rule is first looked for | |||||
| 26 | 25 25 25 | 4782 58356 1001 | use HTML::Entities; | |||
| 27 | 25 25 25 | 6773 2225254 515 | use Object::Configure 0.13; | |||
| 28 | 25 25 25 | 100 127 385 | use Params::Get 0.04; | |||
| 29 | 25 25 25 | 42 26 453 | use Readonly; | |||
| 30 | 25 25 25 | 43 17 438 | use Scalar::Util qw(blessed reftype); | |||
| 31 | 25 25 25 | 36 19 70 | use Time::HiRes; | |||
| 32 | ||||||
| 33 - 41 | =head1 NAME Geo::Coder::List - Call many Geo-Coders =head1 VERSION Version 0.39 =cut | |||||
| 42 | ||||||
| 43 | our $VERSION = '0.39'; | |||||
| 44 | ||||||
| 45 | # ââ Module-level constants (not user-configurable) ââââââââââââââââââââââââââââ | |||||
| 46 | ||||||
| 47 | # Default verbosity: 0 = silent, 1 = basic trace, 2 = full Data::Dumper dumps | |||||
| 48 | 25 25 25 | 979 19 649 | use constant DEBUG_DEFAULT => 0; | |||
| 49 | ||||||
| 50 | # Internal sentinel class: a cached not-found result stored in L1. | |||||
| 51 | # Using a blessed ref lets _cache() distinguish "never looked up" from | |||||
| 52 | # "looked up and found nothing" without requiring a special undef convention. | |||||
| 53 | 25 25 25 | 38 20 60535 | use constant _NOT_FOUND_CLASS => __PACKAGE__ . '::_NotFound'; | |||
| 54 | ||||||
| 55 | # The singleton value stored in L1 when a location is confirmed missing | |||||
| 56 | my $NOT_FOUND_SENTINEL = bless {}, _NOT_FOUND_CLASS; | |||||
| 57 | ||||||
| 58 | # String placed in the 'geocoder' field of a result served from cache | |||||
| 59 | Readonly::Scalar my $CACHE_SOURCE => 'cache'; | |||||
| 60 | ||||||
| 61 | # A latlng argument to reverse_geocode(): two decimal numbers and a comma. | |||||
| 62 | # The classes are spelt out rather than written \d and \s: those match | |||||
| 63 | # Arabic-Indic and other Unicode digits, which no backend reads as numbers, | |||||
| 64 | # and the /a flag that would restrict them needs Perl 5.14. | |||||
| 65 | Readonly::Scalar my $LATLNG_RE => qr/^[ \t]*[-+]?(?:[0-9]*\.?[0-9]+|[0-9]+\.?[0-9]*)[ \t]*,[ \t]*[-+]?(?:[0-9]*\.?[0-9]+|[0-9]+\.?[0-9]*)[ \t]*$/; | |||||
| 66 | ||||||
| 67 | # String used as the 'result' value in log entries for a geocoder miss | |||||
| 68 | Readonly::Scalar my $RESULT_NONE => 'not found'; | |||||
| 69 | ||||||
| 70 | # ââ Configurable defaults âââââââââââââââââââââââââââââââââââââââââââââââââââââ | |||||
| 71 | # | |||||
| 72 | # Any key here can be overridden at run time via an environment variable named | |||||
| 73 | # GEO__CODER__LIST__<key>, which Object::Configure reads automatically in new(). | |||||
| 74 | # Example: | |||||
| 75 | # export GEO__CODER__LIST__cache_hit_duration='7 days' | |||||
| 76 | ||||||
| 77 | my %config = ( | |||||
| 78 | debug => DEBUG_DEFAULT, | |||||
| 79 | # How long a confirmed location stays in the L2 cache | |||||
| 80 | cache_hit_duration => '1 month', | |||||
| 81 | # How long a transient/partial failure is cached (retry tomorrow) | |||||
| 82 | cache_part_duration => '1 day', | |||||
| 83 | # How long a definite not-found is cached (place probably does not exist) | |||||
| 84 | cache_miss_duration => '1 week', | |||||
| 85 | ); | |||||
| 86 | ||||||
| 87 | # ============================================================================= | |||||
| 88 | # PUBLIC API | |||||
| 89 | # ============================================================================= | |||||
| 90 | ||||||
| 91 - 349 | =head1 SYNOPSIS
use Geo::Coder::List;
use Geo::Coder::OSM;
# 1. The simplest use: one list, one geocoder
my $list = Geo::Coder::List->new()->push(Geo::Coder::OSM->new());
my $place = $list->geocode('10 Downing St, London, UK');
if($place) {
print "Latitude: $place->{geometry}{location}{lat}\n";
print "Longitude: $place->{geometry}{location}{lng}\n";
} else {
print "Not found\n";
}
# 2. Several geocoders, tried in order. Geo::Coder::CA is asked only
# about places in Canada or the USA, and at most 1000 times.
use Geo::Coder::CA;
my $chain = Geo::Coder::List->new()
->push({ regex => qr/(Canada|USA)$/, geocoder => Geo::Coder::CA->new(), limit => 1000 })
->push(Geo::Coder::OSM->new());
my $ottawa = $chain->geocode('Ottawa, ON, Canada'); # asks CA first
my $paris = $chain->geocode('Paris, France'); # CA is skipped
# 3. A cache that lives longer than the object (any CHI driver works)
use CHI;
my $cached = Geo::Coder::List->new(
cache => CHI->new(driver => 'File', root_dir => '/var/cache/geocode'),
)->push(Geo::Coder::OSM->new());
# 4. Every candidate, not only the best one: call geocode() in list context
my @candidates = $list->geocode('Springfield, USA');
print scalar(@candidates), " places are called Springfield\n";
# 5. From a position to an address
my $address = $list->reverse_geocode(latlng => '51.5034,-0.1276');
print "$address\n" if defined($address);
# 6. Bad input is fatal, so check it when the text comes from a user
my $text_from_a_form = $ARGV[0];
my $result = eval { $list->geocode($text_from_a_form) };
warn "Not a place: $@" if $@;
# 7. See what happened, then clear the record
$list->geocode('Rome, Italy');
for my $entry (@{ $list->log() }) {
printf "%s: %s in %.3fs\n", $entry->{location}, $entry->{geocoder}, $entry->{timetaken};
}
$list->flush();
=head1 DESCRIPTION
L<Geo::Coder::All> and L<Geo::Coder::Many> are great modules but neither
quite does what I want.
A I<geocoder> turns the name of a place, such as C<'Paris, France'>, into
a position on the Earth (a latitude and a longitude). There are many
geocoding services. Each one knows some places better than others, each
one has its own limits, and each one returns its answer in its own format.
C<Geo::Coder::List> lets you use many geocoders as if they were one:
=over 4
=item * B<A chain.>
You add geocoders with C<push()>. When you ask for a place, the geocoders
are asked one at a time, in the order you added them. The first one that
gives a usable answer wins, and the others are not asked.
=item * B<Routing.>
A geocoder can be given a regular expression. It is then asked only
about places whose names match it.
=item * B<Limits.>
A geocoder can be given a limit: the number of questions it may answer.
When the limit is used up, the geocoder is skipped.
=item * B<One answer format.>
Every answer is changed into the same structure, whichever service gave
it. The structure is the one used by L<HTML::GoogleMaps::V3> and
L<HTML::OSM>:
$result->{geometry}{location}{lat} # latitude
$result->{geometry}{location}{lng} # longitude
$result->{lat} # the same latitude, for convenience
$result->{lng} # the same longitude, for convenience
$result->{lon} # the same longitude, another common name
$result->{geocoder} # the geocoder object that answered,
# or the string 'cache'
=item * B<A cache in two levels.>
Level 1 (L1) is a hash inside the object. It is always used, and it lasts
as long as the object. Level 2 (L2) is optional. It can be a plain hash
or any object with C<get()> and C<set()> methods, such as a L<CHI> cache.
L2 can last longer than the object and can be shared between programs.
How long L2 keeps an answer depends on the answer:
found 1 month (cache_hit_duration)
found, but no latitude 1 day (cache_part_duration)
not found 1 week (cache_miss_duration)
A place that was not found is remembered in L1 only, never in L2.
=back
=head1 SUPPORTED ANSWER FORMATS
Each geocoding service answers in its own format. C<geocode()> finds the
latitude and longitude in each of the formats below and copies them into
the common structure. Any geocoder whose answers use one of these formats
works without extra code. The provider's own fields are kept as well.
Service (module) Where the position is found
------------------------------ -----------------------------------------
OpenStreetMap, RandMcNally lat, lon
Bing Maps BestLocation.Coordinates.Latitude/Longitude
Bing Maps (other form) point.coordinates [lat, lng]
geocoder.ca (Geo::Coder::CA) latt, longt
postcodes.io, Geo::Coder::Free latitude, longitude
HERE / Ovi properties.geoLatitude/geoLongitude
DataScienceToolkit results[0].geometry.location.lat/lng
OpenCage results[0].geometry.lat/lng
GeocodeFarm RESULTS[0].COORDINATES.latitude/longitude
US Census Bureau result.addressMatches[0].coordinates.y/x
GeoNames lat, lng
Mapbox features[0].center [lng, lat]
GeoApify features[0].geometry.coordinates [lng, lat]
Google, and anything already geometry.location.lat/lng
in the common structure
Notice that Mapbox and GeoApify give the longitude B<first>.
Some services need special handling, which is done for you:
=over 4
=item * B<Geo::Location::Point.>
A geocoder may answer with a L<Geo::Location::Point> object. Its
C<geometry> is filled in from its own C<lat> and C<lng> (or C<lon>).
=item * B<postcodes.io.>
Its C<local_type> field is copied to C<type>, with the first letter made
lower case.
=item * B<geocoder.xyz.>
Its C<standard.countryname> field is copied to C<address.country>.
=item * B<GeoNames.>
L<Geo::GeoNames> is called with the place name alone, not with
C<location =E<gt> ...>. If it has no C<username>, it fails with
C<lost username>, which is logged, and the next geocoder is asked.
GeoNames puts its answers in arrays; every answer in them is considered.
=item * B<US Census Bureau.>
An answer with no C<addressMatches> means "not found", and the next
geocoder is asked.
=item * B<GeoApify.>
An answer with an empty C<features> list means "not found", and the next
geocoder is asked.
=item * B<Errors.>
An answer with a top-level C<error> key is treated as a refusal, not as a
place. The error is logged, and the next geocoder is asked.
=back
Every answer that went through this process also has a C<debug> key. It
holds the line number, in this module, of the code that read the answer.
It is there to help when a new format is not read correctly.
=head1 SUBROUTINES/METHODS
=head2 new
Creates a new C<Geo::Coder::List> object with no geocoders in it.
Settings can also come from the environment, through
L<Object::Configure>. The variable name is C<GEO__CODER__LIST__> followed
by the setting name. For example, C<GEO__CODER__LIST__carp_on_warn=1>
makes warnings go through L<Carp>, and
C<GEO__CODER__LIST__cache_hit_duration='7 days'> changes how long a found
place stays in L2.
When you call C<new()> on an object that already exists, you get a
I<clone>: a new object with the same settings and the same geocoders,
merged with any arguments you give. The clone has its own list of
geocoders and its own limits, and it starts with an empty log. The clone
B<shares> the L1 and L2 caches with the original.
use Geo::Coder::List;
use CHI;
# With an L2 cache
my $geocoder = Geo::Coder::List->new(
cache => CHI->new(driver => 'Memory', global => 1),
);
# A clone of it that prints what it is doing
my $verbose = $geocoder->new(debug => 1);
Arguments can be given as a list of pairs or as one hash reference.
=over 4
=item * C<cache> - Optional. The L2 cache: a hash reference, or an object
with C<get($key)> and C<set($key, $value, $ttl)> methods.
=item * C<debug> - Optional. C<0> (the default) prints nothing, C<1>
prints what is tried, C<2> also prints every answer in full.
=item * C<cache_hit_duration>, C<cache_part_duration>,
C<cache_miss_duration> - Optional. How long L2 keeps each kind of
answer, in any form your L2 cache understands, such as C<'1 week'>.
=item * C<geocoders> - Optional. An array reference of geocoders, as if
each had been given to C<push()>.
=item * C<logger> - Optional. Where warnings and errors go. See
L<Object::Configure>.
=item * C<carp_on_warn> - Optional. When true, warnings go through
L<Carp>.
=back
If C<new> is called as a function, C<Geo::Coder::List::new()>, with no
arguments, it still returns an object. Called as a function with
arguments, it warns and returns C<undef>.
=head3 API SPECIFICATION
=head4 INPUT
{
cache => { type => 'hashref|object', optional => 1 },
debug => { type => 'integer', optional => 1, min => 0, max => 2 },
cache_hit_duration => { type => 'string', optional => 1 },
cache_part_duration => { type => 'string', optional => 1 },
cache_miss_duration => { type => 'string', optional => 1 },
geocoders => { type => 'arrayref', optional => 1 },
logger => { type => 'object|coderef|arrayref|string', optional => 1 },
carp_on_warn => { type => 'boolean', optional => 1 },
}
=head4 OUTPUT
{
type => 'object',
isa => 'Geo::Coder::List',
}
# undef only when called as Geo::Coder::List::new() with arguments
=cut | |||||
| 350 | ||||||
| 351 | sub new | |||||
| 352 | { | |||||
| 353 | 406 | 1201325 | my $class = shift; | |||
| 354 | 406 | 627 | my $params = Params::Get::get_params(undef, \@_) || {}; | |||
| 355 | ||||||
| 356 | # Handle the rare Geo::Coder::List::new() (function-style) invocation | |||||
| 357 | 406 | 5091 | if(!defined($class)) { | |||
| 358 | 8 8 | 30 27 | if(scalar keys %{$params} > 0) { | |||
| 359 | # Using ::new() with arguments is not supported | |||||
| 360 | 4 | 37 | carp(__PACKAGE__, ' use ->new() not ::new() to instantiate'); | |||
| 361 | 4 | 714 | return; | |||
| 362 | } | |||||
| 363 | # With no arguments there is nothing to lose, so build a default | |||||
| 364 | # object; with arguments the caller has mistaken the calling form and | |||||
| 365 | # is told so above, rather than having the arguments silently ignored | |||||
| 366 | 4 | 3 | $class = __PACKAGE__; | |||
| 367 | } elsif(blessed($class)) { | |||||
| 368 | # Shallow clone merged with new params; log is always fresh so the | |||||
| 369 | # clone starts with an empty event history independent of the original | |||||
| 370 | # The chain is copied, hashref entries included, so pushing onto the | |||||
| 371 | # clone or spending one of its limits leaves the original's alone | |||||
| 372 | 9 9 9 | 9 540 20 | my $clone = bless { %{$class}, %{$params}, log => [] }, ref($class); | |||
| 373 | $clone->{'geocoders'} = [ | |||||
| 374 | 9 5 1 9 | 12 29 1 16 | map { ref($_) eq 'HASH' ? { %{$_} } : $_ } @{$clone->{'geocoders'} // []} | |||
| 375 | ]; | |||||
| 376 | 9 | 17 | return $clone; | |||
| 377 | } | |||||
| 378 | ||||||
| 379 | # Let Object::Configure overlay defaults from environment / config files | |||||
| 380 | 393 | 505 | $params = Object::Configure::configure($class, $params); | |||
| 381 | ||||||
| 382 | # Fill in any %config defaults the caller did not explicitly supply | |||||
| 383 | 393 | 825620 | for my $key (keys %config) { | |||
| 384 | 1572 | 1935 | $params->{$key} //= $config{$key}; | |||
| 385 | } | |||||
| 386 | ||||||
| 387 | # Bless and return; params override scalar defaults but locations/log | |||||
| 388 | # are always initialised fresh so callers cannot inject stale state | |||||
| 389 | return bless { | |||||
| 390 | debug => DEBUG_DEFAULT, | |||||
| 391 | geocoders => [], | |||||
| 392 | 393 393 | 300 1503 | %{$params}, | |||
| 393 | locations => {}, | |||||
| 394 | log => [], | |||||
| 395 | }, $class; | |||||
| 396 | } | |||||
| 397 | ||||||
| 398 | # ============================================================================= | |||||
| 399 | ||||||
| 400 - 457 | =head2 push
Adds one geocoder to the end of the chain. Geocoders are asked in the
order in which they were added.
C<push> returns the object itself, so you can add several geocoders in
one statement.
There are two ways to add a geocoder:
=over 4
=item * B<A geocoder object.>
It is asked about every place.
=item * B<A hash reference>, with these keys:
=over 4
=item * C<geocoder> - Required. The geocoder object.
=item * C<regex> - Optional. A regular expression, made with C<qr//>.
The geocoder is asked only about places whose names match it.
=item * C<limit> - Optional. A whole number. The geocoder answers at
most this many questions. Each question it is asked makes the number one
smaller. When the number reaches C<0>, the geocoder is skipped. A place
that does not match C<regex> does not use up the limit.
=back
=back
my $list = Geo::Coder::List->new()
->push({ regex => qr/USA$/, geocoder => Geo::Coder::US::Census->new(), limit => 100 })
->push(Geo::Coder::OSM->new());
C<push> croaks if C<$geocoder> is missing or C<undef>.
=head3 API SPECIFICATION
=head4 INPUT
{
geocoder => { type => 'object|hashref', optional => 0 },
}
# A hashref geocoder holds the keys geocoder (an object, required),
# regex (a Regexp, optional) and limit (an integer, optional)
=head4 OUTPUT
{
type => 'object',
isa => 'Geo::Coder::List',
}
=cut | |||||
| 458 | ||||||
| 459 | sub push | |||||
| 460 | { | |||||
| 461 | # Deliberately NOT using Params::Get here: passing the hashref through it | |||||
| 462 | # would stringify the compiled qr// object, destroying the regex. | |||||
| 463 | 308 | 2315 | my ($self, $geocoder) = @_; | |||
| 464 | ||||||
| 465 | # A geocoder argument is mandatory | |||||
| 466 | 308 | 324 | croak(__PACKAGE__, '::push: Usage: ($geocoder)') unless defined($geocoder); | |||
| 467 | ||||||
| 468 | # Append to the ordered chain and return $self for chaining | |||||
| 469 | 303 303 | 144 252 | CORE::push @{$self->{geocoders}}, $geocoder; | |||
| 470 | ||||||
| 471 | 303 | 323 | return $self; | |||
| 472 | } | |||||
| 473 | ||||||
| 474 | # The rule when it is installed, and the one thing that can be said without it. | |||||
| 475 | # A reference is not a location however it would print: when the rule is there it | |||||
| 476 | # says so, and when it is not this refuses it - the hole this module used to have | |||||
| 477 | # was testing the string a reference turns into. Whether a string of digits is a | |||||
| 478 | # place is the rule's question, so without the rule it is not asked: the string | |||||
| 479 | # is looked up, as it was before the rule existed. | |||||
| 480 | sub _could_be_a_location { | |||||
| 481 | 250 | 7739 | my $location = shift; | |||
| 482 | ||||||
| 483 | 250 | 197 | if(!defined($HAVE_RULE)) { | |||
| 484 | 7 | 6 | $HAVE_RULE = eval { | |||
| 485 | 7 | 1890 | require Geo::Location::Predicate; | |||
| 486 | 7 | 1213 | Geo::Location::Predicate->can('_valid_location') ? 1 : 0; | |||
| 487 | } ? 1 : 0; | |||||
| 488 | } | |||||
| 489 | ||||||
| 490 | # When the rule is installed it decides everything, references included, so | |||||
| 491 | # that this module cannot be stricter or looser than the facade. Without it | |||||
| 492 | # there are two answers this module can give on its own. | |||||
| 493 | 250 | 374 | return Geo::Location::Predicate::_valid_location($location) ? 1 : 0 if $HAVE_RULE; | |||
| 494 | ||||||
| 495 | 0 | 0 | return 0 if ref($location); | |||
| 496 | 0 | 0 | return 0 unless defined($location) && length($location); | |||
| 497 | 0 | 0 | return 1; | |||
| 498 | } | |||||
| 499 | ||||||
| 500 | # ============================================================================= | |||||
| 501 | ||||||
| 502 - 595 | =head2 geocode
Finds the position of a place from its name.
my $result = $list->geocode(location => 'Paris, France');
my $result = $list->geocode('Paris, France'); # the same
my $result = $list->geocode({ location => 'Paris, France' }); # the same
if($result) {
printf "lat=%.4f lng=%.4f from %s\n",
$result->{geometry}{location}{lat},
$result->{geometry}{location}{lng},
ref($result->{geocoder}) || $result->{geocoder};
}
These are the steps, in order:
=over 4
=item 1. If C<location> is missing, C<geocode> croaks. If it is
C<undef> or an empty string, C<geocode> warns and returns nothing.
=item 2. Runs of white space are made into one space, and HTML entities
are decoded: C<'Café'> becomes C<'Caf'> followed by the single
character e-acute. A reference to a string is treated as the string.
=item 3. The text is checked. A reference is never a place. When
L<Geo::Location::Predicate> is installed, it decides what else is not a
place; for example, a string of only digits is not. If the text is not a
place, C<geocode> croaks. The check is done after step 2, so
C<'12'> is refused as the digits C<'12'>.
=item 4. The cache is checked: first L1, then L2. If the place is there,
the cached answer is returned, with C<geocoder> set to C<'cache'>. If L1
remembers that the place was not found, nothing is returned and no
geocoder is asked.
=item 5. Each geocoder in the chain is asked in turn. A geocoder is
skipped if the place does not match its C<regex>, or if its C<limit> is
used up. If a geocoder dies, the error is logged, a warning is given, and
the next geocoder is asked.
=item 6. The first answer that has both a latitude and a longitude wins.
It is changed into the common structure (see L</DESCRIPTION>), stored in
the cache, and returned.
=item 7. If no geocoder gives a usable answer, the place is remembered as
"not found" in L1, and nothing is returned.
=back
B<Scalar context> returns one hash reference, or C<undef>.
B<List context> returns every answer from the geocoder that won, or an
empty list. This is useful when a name, such as C<'Springfield'>, belongs
to many places. Answers that the geocoder returned inside an array are
taken out of it, so the list never contains array references. Answers
without a position may be in the list too; check C<geometry> before you
use each one.
my @all = $list->geocode('Springfield, USA');
The C<geocoder> key of an answer holds the geocoder object that gave it.
When the answer came from the cache, it holds the string C<'cache'>.
=head3 API SPECIFICATION
=head4 INPUT
{
location => { type => 'string', optional => 0, min => 1 },
}
=head4 OUTPUT
{
type => 'hashref',
optional => 1,
}
# Return::Set schema, scalar context: a hashref, or undef when not found
# {
# geometry => { location => { lat => Num, lng => Num } },
# lat => Num,
# lng => Num,
# lon => Num, # the same as lng
# geocoder => OBJECT | 'cache',
# debug => Int, # line that read the answer
# ... # fields from the provider
# }
#
# List context: a list of hashrefs as above, or an empty list
=cut | |||||
| 596 | ||||||
| 597 | sub geocode { | |||||
| 598 | 247 | 17791 | my $self = shift; | |||
| 599 | ||||||
| 600 | # The geocoders are called inside eval; the caller's $@ is not ours to change | |||||
| 601 | 247 | 136 | local $@; | |||
| 602 | ||||||
| 603 | # Params::Get enforces 'location'; calling geocode() with no args causes | |||||
| 604 | # get_params itself to croak with "Usage:" matching t/carp.t expectations | |||||
| 605 | 247 | 277 | my $params = Params::Get::get_params('location', @_); | |||
| 606 | 245 | 2753 | if(!defined($params)) { | |||
| 607 | 0 | 0 | $self->_error(__PACKAGE__, ' usage: geocode(location => $location)'); | |||
| 608 | 0 | 0 | return; | |||
| 609 | } | |||||
| 610 | ||||||
| 611 | 245 | 156 | my $location = $params->{'location'}; | |||
| 612 | ||||||
| 613 | # A scalar reference is one way of passing the string it points at, and | |||||
| 614 | # Params::Get dereferences it when it is the only argument. Do it here as | |||||
| 615 | # well, in every calling form, so that it means the string however the | |||||
| 616 | # method is called. | |||||
| 617 | 245 2 | 202 3 | $location = ${$location} if ref($location) eq 'SCALAR'; | |||
| 618 | ||||||
| 619 | # Reject empty or whitespace-only location strings | |||||
| 620 | 245 | 426 | if((!defined($location)) || (length($location) == 0)) { | |||
| 621 | 6 | 9 | $self->_warn(__PACKAGE__, ' usage: geocode(location => $location)'); | |||
| 622 | 6 | 2516 | return; | |||
| 623 | } | |||||
| 624 | ||||||
| 625 | # Collapse runs of whitespace and expand any HTML entities before the rule | |||||
| 626 | # is asked: '12' is the string of digits '12' once decoded, and it | |||||
| 627 | # is the decoded string that is looked up. A reference is left as it is | |||||
| 628 | # for the rule to refuse. | |||||
| 629 | 239 | 190 | if(!ref($location)) { | |||
| 630 | 228 | 1976 | $location =~ s/\s\s+/ /g; | |||
| 631 | 228 | 874 | $location = decode_entities($location); | |||
| 632 | } | |||||
| 633 | ||||||
| 634 | # What counts as a location is Geo::Location::Predicate's answer, not a | |||||
| 635 | # second copy of it here: a reference is not a location however it would print, | |||||
| 636 | # and a string of digits is what a numeric column holds when the wrong one has | |||||
| 637 | # been read. | |||||
| 638 | 239 | 214 | if(!_could_be_a_location($location)) { | |||
| 639 | 18 | 82 | $self->_error('Usage: ', __PACKAGE__, ': invalid input to geocode(), ', | |||
| 640 | ref($location) ? 'a reference is not a location' : $location); | |||||
| 641 | 0 | 0 | return; | |||
| 642 | } | |||||
| 643 | ||||||
| 644 | # Propagate the cleaned-up string so geocoders also receive the decoded form | |||||
| 645 | 221 | 908 | $params->{'location'} = $location; | |||
| 646 | ||||||
| 647 | 221 | 379 | $self->_debug("location: $location\n"); | |||
| 648 | ||||||
| 649 | # Capture the caller's line number once for all log entries in this call | |||||
| 650 | 221 | 213 | my @call_details = caller(0); | |||
| 651 | ||||||
| 652 | # ââ L1 / L2 cache lookup ââââââââââââââââââââââââââââââââââââââââââââââ | |||||
| 653 | ||||||
| 654 | # _cache() returns undef for both "not in cache" and "cached not-found"; | |||||
| 655 | # the not-found sentinel is handled internally so callers see undef either way | |||||
| 656 | 221 | 1544 | my $cached = $self->_cache($location); | |||
| 657 | ||||||
| 658 | # Only a reference is a geocode result. A plain string in the cache under | |||||
| 659 | # this key - a reverse_geocode() address, or a corrupted store - is not, | |||||
| 660 | # and is looked up again rather than returned as if it were one. | |||||
| 661 | 221 | 229 | if(defined($cached) && !ref($cached)) { | |||
| 662 | 1 | 1 | undef $cached; | |||
| 663 | } | |||||
| 664 | ||||||
| 665 | 221 | 159 | if(defined $cached) { | |||
| 666 | # A defined value means we have a genuine cached positive result | |||||
| 667 | 28 5 | 30 5 | my @rc = ref($cached) eq 'ARRAY' ? @{$cached} : ($cached); | |||
| 668 | ||||||
| 669 | # Mark every element as coming from cache. Shallow-copy HASH results | |||||
| 670 | # first so that neither the L1 cache entry nor any caller-held reference | |||||
| 671 | # to the same hashref is mutated in place. | |||||
| 672 | 28 | 26 | for my $r (@rc) { | |||
| 673 | 28 | 19 | next unless ref($r); | |||
| 674 | 28 26 | 33 57 | $r = { %{$r} } if ref($r) eq 'HASH'; | |||
| 675 | 28 | 33 | $r->{'geocoder'} = $CACHE_SOURCE; | |||
| 676 | } | |||||
| 677 | ||||||
| 678 | # Scalar context: return the first (and usually only) element | |||||
| 679 | 28 | 27 | if(!wantarray) { | |||
| 680 | 22 | 10 | my $rc = $rc[0]; | |||
| 681 | 22 22 | 14 55 | CORE::push @{$self->{'log'}}, { | |||
| 682 | line => $call_details[2], | |||||
| 683 | location => $location, | |||||
| 684 | timetaken => 0, | |||||
| 685 | geocoder => $CACHE_SOURCE, | |||||
| 686 | wantarray => 0, | |||||
| 687 | result => $rc, | |||||
| 688 | }; | |||||
| 689 | 22 | 22 | $self->_debug(__PACKAGE__, ': ', __LINE__, ": cached\n"); | |||
| 690 | 22 | 36 | return $rc; | |||
| 691 | } | |||||
| 692 | ||||||
| 693 | # List context: return all cached candidates | |||||
| 694 | 6 6 | 6 14 | CORE::push @{$self->{'log'}}, { | |||
| 695 | line => $call_details[2], | |||||
| 696 | location => $location, | |||||
| 697 | timetaken => 0, | |||||
| 698 | geocoder => $CACHE_SOURCE, | |||||
| 699 | wantarray => 1, | |||||
| 700 | result => \@rc, | |||||
| 701 | }; | |||||
| 702 | 6 | 6 | $self->_debug(__PACKAGE__, ': ', __LINE__, ": cached\n"); | |||
| 703 | ||||||
| 704 | # Determine if every element is empty; if so return nothing | |||||
| 705 | 6 | 2 | my $allempty = 1; | |||
| 706 | 6 | 6 | for my $r (@rc) { | |||
| 707 | 6 | 8 | if(ref($r) eq 'HASH') { | |||
| 708 | 5 | 6 | $allempty = 0 if defined $r->{geometry}{location}{lat}; | |||
| 709 | } elsif(ref($r) eq 'Geo::Location::Point') { | |||||
| 710 | 1 | 1 | $allempty = 0; | |||
| 711 | } | |||||
| 712 | } | |||||
| 713 | 6 | 6 | return if $allempty; | |||
| 714 | 4 | 8 | return @rc; | |||
| 715 | } | |||||
| 716 | ||||||
| 717 | # Also check if this location is cached as a definite not-found in L1, | |||||
| 718 | # without going through _cache() (which masks the sentinel as undef) | |||||
| 719 | 193 | 160 | if(exists $self->{'locations'}{$location}) { | |||
| 720 | 6 | 5 | my $stored = $self->{'locations'}{$location}; | |||
| 721 | 6 | 16 | if(ref($stored) && ref($stored) eq _NOT_FOUND_CLASS) { | |||
| 722 | 6 | 8 | $self->_debug("No matches (cached)\n"); | |||
| 723 | 6 | 12 | return wantarray ? () : undef; | |||
| 724 | } | |||||
| 725 | } | |||||
| 726 | ||||||
| 727 | # ââ Try each geocoder in turn âââââââââââââââââââââââââââââââââââââââââ | |||||
| 728 | ||||||
| 729 | 187 187 | 85 170 | ENCODER: foreach my $g (@{$self->{geocoders}}) { | |||
| 730 | 193 | 100 | my $geocoder = $g; | |||
| 731 | ||||||
| 732 | # Unpack a hashref entry and apply regex / limit guards | |||||
| 733 | 193 | 192 | if(ref($geocoder) eq 'HASH') { | |||
| 734 | # Skip this entry if the location does not match its regex. This | |||||
| 735 | # comes before the limit so that a location the geocoder is never | |||||
| 736 | # asked about does not spend one of its queries. | |||||
| 737 | 19 | 25 | if(my $regex = $geocoder->{'regex'}) { | |||
| 738 | 6 | 16 | $self->_debug('consider ', ref($geocoder->{geocoder}), ": $regex\n"); | |||
| 739 | 6 | 18 | if($location !~ $regex) { | |||
| 740 | 4 | 6 | next; | |||
| 741 | } | |||||
| 742 | } | |||||
| 743 | ||||||
| 744 | # Decrement and check the per-geocoder query limit | |||||
| 745 | 15 | 31 | if(exists($geocoder->{'limit'}) && defined(my $limit = $geocoder->{'limit'})) { | |||
| 746 | 14 | 23 | $self->_debug("limit: $limit\n"); | |||
| 747 | 14 | 19 | if($limit <= 0) { | |||
| 748 | 8 | 8 | next; | |||
| 749 | } | |||||
| 750 | 6 | 4 | $geocoder->{'limit'}--; | |||
| 751 | } | |||||
| 752 | ||||||
| 753 | # Unwrap the actual geocoder object from the hashref | |||||
| 754 | 7 | 7 | $geocoder = $g->{'geocoder'}; | |||
| 755 | } | |||||
| 756 | ||||||
| 757 | # Start timing before the network call | |||||
| 758 | 181 | 77 | my @rc; | |||
| 759 | 181 | 641 | my $timetaken = Time::HiRes::time(); | |||
| 760 | ||||||
| 761 | 181 | 524 | eval { | |||
| 762 | # Geo::GeoNames uses a positional argument, not a hash | |||||
| 763 | 181 | 179 | $self->_debug('trying ', ref($geocoder), "\n"); | |||
| 764 | 181 | 161 | if(ref($geocoder) eq 'Geo::GeoNames') { | |||
| 765 | 2 | 2 | $self->_debug('username => ', $geocoder->username(), "\n"); | |||
| 766 | 2 | 1 | die 'lost username' if(!defined($geocoder->username())); | |||
| 767 | 1 | 2 | @rc = $geocoder->geocode($location); | |||
| 768 | } else { | |||||
| 769 | 179 179 | 86 277 | @rc = $geocoder->geocode(%{$params}); | |||
| 770 | } | |||||
| 771 | }; | |||||
| 772 | ||||||
| 773 | 181 | 1360 | if($@) { | |||
| 774 | # Log the failure and move on; do not abort the whole chain | |||||
| 775 | 12 | 40 | my $log = { | |||
| 776 | line => $call_details[2], | |||||
| 777 | location => $location, | |||||
| 778 | geocoder => ref($geocoder), | |||||
| 779 | timetaken => Time::HiRes::time() - $timetaken, | |||||
| 780 | wantarray => wantarray, | |||||
| 781 | error => $@, | |||||
| 782 | }; | |||||
| 783 | 12 12 | 8 12 | CORE::push @{$self->{'log'}}, $log; | |||
| 784 | 12 | 23 | $self->_warn(ref($geocoder), " '$location': $@"); | |||
| 785 | 12 | 4130 | next ENCODER; | |||
| 786 | } | |||||
| 787 | ||||||
| 788 | 169 | 149 | $timetaken = Time::HiRes::time() - $timetaken; | |||
| 789 | ||||||
| 790 | # Geo::Coder::US::Census sometimes returns a truthy but empty result | |||||
| 791 | 169 | 174 | if((ref($geocoder) eq 'Geo::Coder::US::Census') && | |||
| 792 | !(defined($rc[0]->{result}{addressMatches}[0]->{coordinates}{y}))) { | |||||
| 793 | 2 | 5 | my $log = { | |||
| 794 | line => $call_details[2], | |||||
| 795 | location => $location, | |||||
| 796 | timetaken => $timetaken, | |||||
| 797 | geocoder => ref($geocoder), | |||||
| 798 | wantarray => wantarray, | |||||
| 799 | result => $RESULT_NONE, | |||||
| 800 | }; | |||||
| 801 | 2 2 | 2 2 | CORE::push @{$self->{'log'}}, $log; | |||
| 802 | 2 | 3 | next ENCODER; | |||
| 803 | } | |||||
| 804 | ||||||
| 805 | # Geo::GeoNames wraps its results in arrays. Every element of such an | |||||
| 806 | # array is a candidate, so they are unwrapped into one list here, and a | |||||
| 807 | # wrapped empty hash is then caught by the check below like any other | |||||
| 808 | 167 20153 5 | 123 9447 8 | @rc = map { ref($_) eq 'ARRAY' ? @{$_} : $_ } @rc; | |||
| 809 | ||||||
| 810 | # Reject empty result sets and trivially empty hashes | |||||
| 811 | 167 | 255 | if((scalar(@rc) == 0) || | |||
| 812 | 130 | 178 | ((ref($rc[0]) eq 'HASH') && (scalar(keys %{$rc[0]}) == 0))) { | |||
| 813 | 22 | 53 | my $log = { | |||
| 814 | line => $call_details[2], | |||||
| 815 | location => $location, | |||||
| 816 | timetaken => $timetaken, | |||||
| 817 | geocoder => ref($geocoder), | |||||
| 818 | wantarray => wantarray, | |||||
| 819 | result => $RESULT_NONE, | |||||
| 820 | }; | |||||
| 821 | 22 22 | 9 23 | CORE::push @{$self->{'log'}}, $log; | |||
| 822 | 22 | 23 | next ENCODER; | |||
| 823 | } | |||||
| 824 | ||||||
| 825 | # ââ Normalise each candidate result ââââââââââââââââââââââââââââââ | |||||
| 826 | ||||||
| 827 | # Track which element was successfully normalised so we return it, | |||||
| 828 | # not blindly return $rc[0] when a later element was the good one | |||||
| 829 | 145 | 74 | my $good_result; | |||
| 830 | ||||||
| 831 | 145 | 100 | POSSIBLE_LOCATION: foreach my $l (@rc) { | |||
| 832 | # Skip undefined or empty-string candidates | |||||
| 833 | 152 | 224 | if((!defined($l)) || ($l eq '')) { | |||
| 834 | 5 | 12 | my $log = { | |||
| 835 | line => $call_details[2], | |||||
| 836 | location => $location, | |||||
| 837 | timetaken => $timetaken, | |||||
| 838 | geocoder => ref($geocoder), | |||||
| 839 | wantarray => wantarray, | |||||
| 840 | result => $RESULT_NONE, | |||||
| 841 | }; | |||||
| 842 | 5 5 | 5 3 | CORE::push @{$self->{'log'}}, $log; | |||
| 843 | 5 | 9 | next ENCODER; | |||
| 844 | } | |||||
| 845 | ||||||
| 846 | # Skip bare scalars (e.g. integer 0, plain strings) that are | |||||
| 847 | # not references; they cannot be hash-dereferenced below | |||||
| 848 | 147 | 127 | next unless ref($l); | |||
| 849 | ||||||
| 850 | # Stamp the source geocoder on the result before normalisation | |||||
| 851 | 139 | 133 | $l->{'geocoder'} = ref($geocoder); | |||
| 852 | ||||||
| 853 | $self->_debug(ref($geocoder), ': ', | |||||
| 854 | 139 | 134 | Data::Dumper->new([\$l])->Dump()) if($self->{'debug'} >= 2); | |||
| 855 | ||||||
| 856 | # Geo::Location::Point objects carry their own accessors; | |||||
| 857 | # upgrade the geocoder field and populate the canonical geometry | |||||
| 858 | # structure so callers can rely on geometry.location.{lat,lng} | |||||
| 859 | 139 | 131 | if(ref($l) eq 'Geo::Location::Point') { | |||
| 860 | 4 | 2 | $l->{'geocoder'} = $geocoder; | |||
| 861 | ||||||
| 862 | # Populate canonical geometry structure from the GLP's own fields | |||||
| 863 | 4 | 11 | if(!defined($l->{geometry}{location}{lat}) && defined($l->{lat})) { | |||
| 864 | 3 | 2 | $l->{geometry}{location}{lat} = $l->{lat}; | |||
| 865 | 3 | 5 | $l->{geometry}{location}{lng} = $l->{lng} // $l->{lon}; | |||
| 866 | } | |||||
| 867 | ||||||
| 868 | # Convenience aliases (idempotent if already set by GLP) | |||||
| 869 | 4 | 7 | $l->{'lat'} //= $l->{geometry}{location}{lat}; | |||
| 870 | 4 | 4 | $l->{'lng'} //= $l->{geometry}{location}{lng}; | |||
| 871 | 4 | 8 | $l->{'lon'} //= $l->{geometry}{location}{lng}; | |||
| 872 | ||||||
| 873 | 4 4 | 3 10 | CORE::push @{$self->{'log'}}, { | |||
| 874 | line => $call_details[2], | |||||
| 875 | location => $location, | |||||
| 876 | timetaken => $timetaken, | |||||
| 877 | geocoder => ref($geocoder), | |||||
| 878 | wantarray => wantarray, | |||||
| 879 | result => $l, | |||||
| 880 | }; | |||||
| 881 | 4 | 7 | $good_result = $l; | |||
| 882 | 4 | 3 | last POSSIBLE_LOCATION; | |||
| 883 | } | |||||
| 884 | ||||||
| 885 | # Only HASH results need normalisation | |||||
| 886 | 135 | 112 | next if(ref($l) ne 'HASH'); | |||
| 887 | ||||||
| 888 | 135 | 102 | if($l->{'error'}) { | |||
| 889 | # A top-level 'error' key signals a provider-level failure | |||||
| 890 | my $log = { | |||||
| 891 | line => $call_details[2], | |||||
| 892 | location => $location, | |||||
| 893 | timetaken => $timetaken, | |||||
| 894 | geocoder => ref($geocoder), | |||||
| 895 | wantarray => wantarray, | |||||
| 896 | 2 | 5 | error => $l->{'error'}, | |||
| 897 | }; | |||||
| 898 | 2 2 | 2 3 | CORE::push @{$self->{'log'}}, $log; | |||
| 899 | 2 | 4 | next ENCODER; | |||
| 900 | } else { | |||||
| 901 | # Map provider-specific fields to the canonical geometry structure | |||||
| 902 | 133 | 165 | if(!defined($l->{geometry}{location}{lat})) { | |||
| 903 | 127 | 64 | my ($lat, $long); | |||
| 904 | ||||||
| 905 | 127 | 279 | if(defined($l->{lat}) && defined($l->{lon})) { | |||
| 906 | # OSM / RandMcNally: top-level lat/lon fields | |||||
| 907 | 99 | 63 | $lat = $l->{lat}; | |||
| 908 | 99 | 65 | $long = $l->{lon}; | |||
| 909 | 99 | 71 | $l->{'debug'} = __LINE__; | |||
| 910 | } elsif($l->{BestLocation}) { | |||||
| 911 | # Bing Maps: BestLocation.Coordinates.{Latitude,Longitude} | |||||
| 912 | 2 | 2 | $lat = $l->{BestLocation}->{Coordinates}->{Latitude}; | |||
| 913 | 2 | 2 | $long = $l->{BestLocation}->{Coordinates}->{Longitude}; | |||
| 914 | 2 | 2 | $l->{'debug'} = __LINE__; | |||
| 915 | } elsif($l->{point}) { | |||||
| 916 | # Bing Maps alternative: point.coordinates[lat, lng] | |||||
| 917 | 3 | 4 | $lat = $l->{point}->{coordinates}[0]; | |||
| 918 | 3 | 2 | $long = $l->{point}->{coordinates}[1]; | |||
| 919 | 3 | 4 | $l->{'debug'} = __LINE__; | |||
| 920 | } elsif(defined($l->{latt})) { | |||||
| 921 | # geocoder.ca: latt / longt fields | |||||
| 922 | 2 | 2 | $lat = $l->{latt}; | |||
| 923 | 2 | 2 | $long = $l->{longt}; | |||
| 924 | 2 | 2 | $l->{'debug'} = __LINE__; | |||
| 925 | } elsif(defined($l->{latitude})) { | |||||
| 926 | # postcodes.io, Geo::Coder::Free: latitude / longitude | |||||
| 927 | 3 | 2 | $lat = $l->{latitude}; | |||
| 928 | 3 | 4 | $long = $l->{longitude}; | |||
| 929 | 3 | 5 | if(my $type = $l->{'local_type'}) { | |||
| 930 | # Carry the local_type hint forward as a normalised 'type' | |||||
| 931 | 1 | 2 | $l->{'type'} = lcfirst($type); | |||
| 932 | } | |||||
| 933 | 3 | 3 | $l->{'debug'} = __LINE__; | |||
| 934 | } elsif(defined($l->{'properties'}{'geoLatitude'})) { | |||||
| 935 | # HERE / Ovi: properties.geoLatitude / geoLongitude | |||||
| 936 | 1 | 1 | $lat = $l->{properties}{geoLatitude}; | |||
| 937 | 1 | 1 | $long = $l->{properties}{geoLongitude}; | |||
| 938 | 1 | 2 | $l->{'debug'} = __LINE__; | |||
| 939 | } elsif($l->{'results'}[0]->{'geometry'}) { | |||||
| 940 | 3 | 5 | if($l->{'results'}[0]->{'geometry'}->{'location'}) { | |||
| 941 | # DataScienceToolkit mirrors the Google Maps shape | |||||
| 942 | 1 | 1 | $lat = $l->{'results'}[0]->{'geometry'}->{'location'}->{'lat'}; | |||
| 943 | 1 | 1 | $long = $l->{'results'}[0]->{'geometry'}->{'location'}->{'lng'}; | |||
| 944 | 1 | 2 | $l->{'debug'} = __LINE__; | |||
| 945 | } else { | |||||
| 946 | # OpenCage places lat/lng directly under geometry | |||||
| 947 | 2 | 3 | $lat = $l->{'results'}[0]->{'geometry'}->{'lat'}; | |||
| 948 | 2 | 1 | $long = $l->{'results'}[0]->{'geometry'}->{'lng'}; | |||
| 949 | 2 | 3 | $l->{'debug'} = __LINE__; | |||
| 950 | } | |||||
| 951 | } elsif($l->{'RESULTS'}) { | |||||
| 952 | # GeoCodeFarm: RESULTS[0].COORDINATES.{latitude,longitude} | |||||
| 953 | 2 | 3 | $lat = $l->{'RESULTS'}[0]{'COORDINATES'}{'latitude'}; | |||
| 954 | 2 | 2 | $long = $l->{'RESULTS'}[0]{'COORDINATES'}{'longitude'}; | |||
| 955 | 2 | 2 | $l->{'debug'} = __LINE__; | |||
| 956 | } elsif(defined($l->{result}{addressMatches}[0]->{coordinates}{y})) { | |||||
| 957 | # US Census Bureau: result.addressMatches[0].coordinates.{y,x} | |||||
| 958 | 1 | 2 | $lat = $l->{result}{addressMatches}[0]->{coordinates}{y}; | |||
| 959 | 1 | 2 | $long = $l->{result}{addressMatches}[0]->{coordinates}{x}; | |||
| 960 | 1 | 1 | $l->{'debug'} = __LINE__; | |||
| 961 | } elsif(defined($l->{lat})) { | |||||
| 962 | # Geo::GeoNames: lat / lng (reached only after lat+lon check fails) | |||||
| 963 | 3 | 4 | $lat = $l->{lat}; | |||
| 964 | 3 | 3 | $long = $l->{lng}; | |||
| 965 | 3 | 4 | $l->{'debug'} = __LINE__; | |||
| 966 | } elsif($l->{features}) { | |||||
| 967 | 4 | 8 | if($l->{features}[0]->{center}) { | |||
| 968 | # Geo::Coder::Mapbox: center is [lng, lat] | |||||
| 969 | 1 | 1 | $lat = $l->{features}[0]->{center}[1]; | |||
| 970 | 1 | 1 | $long = $l->{features}[0]->{center}[0]; | |||
| 971 | 1 | 19 | $l->{'debug'} = __LINE__; | |||
| 972 | } elsif($l->{'features'}[0]{'geometry'}{'coordinates'}) { | |||||
| 973 | # Geo::Coder::GeoApify: coordinates is [lng, lat] | |||||
| 974 | 1 | 1 | $lat = $l->{'features'}[0]{'geometry'}{'coordinates'}[1]; | |||
| 975 | 1 | 1 | $long = $l->{'features'}[0]{'geometry'}{'coordinates'}[0]; | |||
| 976 | 1 | 1 | $l->{'debug'} = __LINE__; | |||
| 977 | } else { | |||||
| 978 | # GeoApify signals not-found via empty features, not an error | |||||
| 979 | 2 | 6 | next ENCODER; | |||
| 980 | } | |||||
| 981 | } else { | |||||
| 982 | 4 | 7 | $l->{'debug'} = __LINE__; | |||
| 983 | } | |||||
| 984 | ||||||
| 985 | 125 | 158 | if(defined($lat) && defined($long)) { | |||
| 986 | # Populate the canonical geometry structure | |||||
| 987 | 121 | 121 | $l->{geometry}{location}{lat} = $lat; | |||
| 988 | 121 | 87 | $l->{geometry}{location}{lng} = $long; | |||
| 989 | # Compatibility aliases expected by callers | |||||
| 990 | 121 | 75 | $l->{'lat'} = $lat; | |||
| 991 | 121 | 580 | $l->{'lon'} = $long; | |||
| 992 | } else { | |||||
| 993 | # No coordinates extracted; clean up any partial data | |||||
| 994 | 4 | 5 | delete $l->{'geometry'}; | |||
| 995 | 4 | 2 | delete $l->{'lat'}; | |||
| 996 | 4 | 3 | delete $l->{'lon'}; | |||
| 997 | } | |||||
| 998 | ||||||
| 999 | # geocoder.xyz provides a country name under 'standard' | |||||
| 1000 | 125 | 194 | if($l->{'standard'}{'countryname'}) { | |||
| 1001 | 1 | 2 | $l->{'address'}{'country'} = $l->{'standard'}{'countryname'}; | |||
| 1002 | } | |||||
| 1003 | } | |||||
| 1004 | ||||||
| 1005 | # A result whose geometry has lat but no lng may still carry the | |||||
| 1006 | # longitude at the top level, some geocoders using the legacy | |||||
| 1007 | # 'long' key for it | |||||
| 1008 | 131 | 125 | if(defined($l->{geometry}{location}{lat})) { | |||
| 1009 | 127 | 123 | $l->{geometry}{location}{lng} //= $l->{'lng'} // $l->{'lon'} // $l->{'long'}; | |||
| 1010 | } | |||||
| 1011 | ||||||
| 1012 | # Only a result with both coordinates is usable; anything less is | |||||
| 1013 | # passed over so that a later element, or the next geocoder, can | |||||
| 1014 | # answer instead | |||||
| 1015 | 131 | 206 | if(defined($l->{geometry}{location}{lat}) && defined($l->{geometry}{location}{lng})) { | |||
| 1016 | $self->_debug($l->{geometry}{location}{lat}, '/', | |||||
| 1017 | 125 | 158 | $l->{geometry}{location}{lng}, "\n"); | |||
| 1018 | ||||||
| 1019 | # Store the geocoder object (not just its name) on the result | |||||
| 1020 | 125 | 99 | $l->{geocoder} = $geocoder; | |||
| 1021 | 125 | 98 | $l->{'lat'} //= $l->{geometry}{location}{lat}; | |||
| 1022 | 125 | 204 | $l->{'lng'} //= $l->{geometry}{location}{lng}; | |||
| 1023 | 125 | 93 | $l->{'lon'} //= $l->{geometry}{location}{lng}; | |||
| 1024 | ||||||
| 1025 | 125 | 290 | my $log = { | |||
| 1026 | line => $call_details[2], | |||||
| 1027 | location => $location, | |||||
| 1028 | timetaken => $timetaken, | |||||
| 1029 | geocoder => ref($geocoder), | |||||
| 1030 | wantarray => wantarray, | |||||
| 1031 | result => $l, | |||||
| 1032 | }; | |||||
| 1033 | 125 125 | 73 97 | CORE::push @{$self->{'log'}}, $log; | |||
| 1034 | ||||||
| 1035 | # Record which element succeeded, then exit the inner loop | |||||
| 1036 | 125 | 109 | $good_result = $l; | |||
| 1037 | 125 | 97 | last POSSIBLE_LOCATION; | |||
| 1038 | } | |||||
| 1039 | } | |||||
| 1040 | } | |||||
| 1041 | ||||||
| 1042 | # Only attempt to return / cache if normalisation actually succeeded | |||||
| 1043 | 136 | 117 | next ENCODER unless defined $good_result; | |||
| 1044 | ||||||
| 1045 | 129 | 149 | $self->_debug('Number of matches from ', ref($geocoder), ': ', | |||
| 1046 | scalar(@rc), "\n"); | |||||
| 1047 | ||||||
| 1048 | 129 | 126 | if($self->{'debug'} >= 2) { | |||
| 1049 | # Use 'local' to avoid permanently altering the global Maxdepth | |||||
| 1050 | 2 | 2 | local $Data::Dumper::Maxdepth = 10; | |||
| 1051 | 2 | 4 | $self->_debug(Data::Dumper->new([\@rc])->Dump()); | |||
| 1052 | } | |||||
| 1053 | ||||||
| 1054 | # Only a result with both coordinates is accepted above, so this can | |||||
| 1055 | # fail only if the normalisation itself is wrong | |||||
| 1056 | 129 | 207 | if((!defined($good_result->{lat})) || (!defined($good_result->{lng}))) { | |||
| 1057 | 0 | 0 | $self->_warn(Data::Dumper->new([\@rc])->Dump()); | |||
| 1058 | 0 | 0 | $self->_error("BUG: '$location': HASH exists but is not sensible"); | |||
| 1059 | } | |||||
| 1060 | ||||||
| 1061 | 129 | 104 | if(wantarray) { | |||
| 1062 | # A bare scalar the loop passed over is not a result | |||||
| 1063 | 9 10015 | 17 3844 | @rc = grep { ref } @rc; | |||
| 1064 | 9 | 13 | $self->_cache($location, \@rc); | |||
| 1065 | 9 | 163 | return @rc; | |||
| 1066 | } | |||||
| 1067 | ||||||
| 1068 | 120 | 122 | $self->_cache($location, $good_result); | |||
| 1069 | 120 | 333 | return $good_result; | |||
| 1070 | } | |||||
| 1071 | ||||||
| 1072 | # ââ No geocoder produced a usable result ââââââââââââââââââââââââââââââ | |||||
| 1073 | ||||||
| 1074 | 58 | 52 | $self->_debug("No matches\n"); | |||
| 1075 | ||||||
| 1076 | # Cache the not-found result so repeated calls do not hammer all backends | |||||
| 1077 | 58 | 58 | $self->_cache($location, undef); | |||
| 1078 | ||||||
| 1079 | 58 | 122 | return wantarray ? () : undef; | |||
| 1080 | } | |||||
| 1081 | ||||||
| 1082 | # ============================================================================= | |||||
| 1083 | ||||||
| 1084 - 1129 | =head2 ua
Gives a user agent (the object that makes the web requests) to every
geocoder in the chain. Use it to set a proxy or a time-out for all of
them at once.
use LWP::UserAgent;
my $ua = LWP::UserAgent->new(timeout => 10);
$ua->env_proxy(1);
$list->ua($ua);
If C<$ua> has both C<clone()> and C<agent()> methods, as
L<LWP::UserAgent> does, each geocoder is given its own copy. The
C<agent> string of each copy is set to the geocoder's class name and
version, such as C<'Geo::Coder::OSM/0.03'>, so each service can see which
module is calling it. Otherwise every geocoder is given C<$ua> itself.
Add the geocoders before you call C<ua>: geocoders added later do not get
the user agent.
There is no way to read the user agent back, because each geocoder may
have a different one.
If C<$ua> is missing or false, nothing changes and nothing is returned.
C<ua> croaks with C<No geocoder found> if an entry in the chain has no
geocoder.
=head3 API SPECIFICATION
=head4 INPUT
{
ua => { type => 'object', optional => 1 },
}
=head4 OUTPUT
{
type => 'object',
optional => 1,
}
# The same $ua that was passed in, or nothing when $ua is false
=cut | |||||
| 1130 | ||||||
| 1131 | sub ua | |||||
| 1132 | { | |||||
| 1133 | 35 | 2225 | my ($self, $ua) = @_; | |||
| 1134 | ||||||
| 1135 | # Nothing to propagate if no UA was supplied | |||||
| 1136 | 35 | 65 | return unless $ua; | |||
| 1137 | ||||||
| 1138 | # Push the UA into every geocoder in the chain | |||||
| 1139 | 28 28 | 16 27 | foreach my $g (@{$self->{geocoders}}) { | |||
| 1140 | 31 | 45 | my $geocoder = (ref($g) eq 'HASH') ? $g->{geocoder} : $g; | |||
| 1141 | # Guard against a misconfigured entry that has no geocoder object | |||||
| 1142 | 31 | 82 | Carp::croak('No geocoder found') unless defined $geocoder; | |||
| 1143 | ||||||
| 1144 | # When the incoming UA supports clone(), create a per-geocoder copy | |||||
| 1145 | # and set the geocoder's own agent string on that copy. | |||||
| 1146 | # Some APIs (e.g. OSM Nominatim) require a specific User-Agent and | |||||
| 1147 | # refuse requests that carry the generic libwww-perl default. | |||||
| 1148 | # The agent string is derived from the geocoder class name and version | |||||
| 1149 | # (e.g. 'Geo::Coder::OSM/0.03') without reading the geocoder's current | |||||
| 1150 | # UA, which would trigger any spy or hook installed on its ua() method. | |||||
| 1151 | 27 | 89 | if($ua->can('clone') && $ua->can('agent')) { | |||
| 1152 | 13 | 17 | my $per_ua = $ua->clone(); | |||
| 1153 | 13 | 36 | my $class = ref($geocoder); | |||
| 1154 | 13 13 | 22 55 | my $version = eval { $geocoder->VERSION() } // ''; | |||
| 1155 | 13 | 23 | $per_ua->agent($version ? "$class/$version" : $class); | |||
| 1156 | 13 | 55 | $geocoder->ua($per_ua); | |||
| 1157 | } else { | |||||
| 1158 | 14 | 16 | $geocoder->ua($ua); | |||
| 1159 | } | |||||
| 1160 | } | |||||
| 1161 | ||||||
| 1162 | # Return the UA so callers can verify what was set (API contract) | |||||
| 1163 | 24 | 50 | return $ua; | |||
| 1164 | } | |||||
| 1165 | ||||||
| 1166 | # ============================================================================= | |||||
| 1167 | ||||||
| 1168 - 1234 | =head2 reverse_geocode
Finds the address of a position: the opposite of C<geocode>.
my $address = $list->reverse_geocode(latlng => '51.5034,-0.1276');
my $address = $list->reverse_geocode('51.5034,-0.1276'); # the same
print "Address: $address\n" if defined($address);
my @addresses = $list->reverse_geocode(latlng => '51.5034,-0.1276');
C<latlng> is the latitude, a comma, and the longitude, in that order. It
must be two plain decimal numbers written with the digits C<0> to C<9>.
Spaces, tabs and a C<+> or C<-> sign are allowed. Anything else, such as
C<'51.5;-0.1'>, C<'1e5,2'> or C<'51.5'>, makes C<reverse_geocode> croak
before any geocoder is asked.
The geocoders are asked in turn, as for C<geocode>. C<limit> is honoured;
C<regex> is not. If a geocoder complains that it does not know the
C<latlng> parameter, it is asked again with only C<lat> and C<lon>. If a
geocoder dies for any other reason, a warning is given and the next
geocoder is asked.
B<Scalar context> returns one address string, or C<undef>.
B<List context> returns every address from the first geocoder that has
any, or an empty list.
An address is taken from the first of these that the answer has:
=over 4
=item * C<display_name> (OpenStreetMap, and others that copy it)
=item * the fields of geocoder.ca: C<stnumber>, C<staddress>, C<city> and
C<prov> for Canada, giving for example
C<'1 Sussex Dr, Ottawa, ON'>; or the C<usa> fields for the United States,
giving for example C<'1600 Pennsylvania Ave, Washington, DC, USA'>
=item * C<features[0].properties.formatted> (GeoApify); only the first
feature is used
=back
If a geocoder returns a plain string, the string is the address.
Addresses are cached under the C<latlng> string, so asking again for the
same position does not ask any geocoder.
=head3 API SPECIFICATION
=head4 INPUT
{
latlng => { type => 'string', optional => 0, matches => qr/^[ \t]*[-+]?(?:[0-9]*\.?[0-9]+|[0-9]+\.?[0-9]*)[ \t]*,[ \t]*[-+]?(?:[0-9]*\.?[0-9]+|[0-9]+\.?[0-9]*)[ \t]*$/ },
}
=head4 OUTPUT
{
type => 'string',
optional => 1,
}
# Scalar context: an address string, or undef when not found
# List context: a list of address strings, or an empty list
=cut | |||||
| 1235 | ||||||
| 1236 | sub reverse_geocode { | |||||
| 1237 | 86 | 9504 | my $self = shift; | |||
| 1238 | ||||||
| 1239 | # The geocoders are called inside eval; the caller's $@ is not ours to change | |||||
| 1240 | 86 | 51 | local $@; | |||
| 1241 | ||||||
| 1242 | 86 | 110 | my $params = Params::Get::get_params('latlng', \@_); | |||
| 1243 | ||||||
| 1244 | 84 | 746 | my $latlng = $params->{'latlng'} or Carp::croak('Usage: reverse_geocode(latlng => $location)'); | |||
| 1245 | ||||||
| 1246 | # Only a numeric pair is a position; anything else would be split into | |||||
| 1247 | # nonsense coordinates and sent to every backend | |||||
| 1248 | 82 | 483 | Carp::croak('Usage: reverse_geocode(latlng => $location)') | |||
| 1249 | if(ref($latlng) || ($latlng !~ $LATLNG_RE)); | |||||
| 1250 | ||||||
| 1251 | # Split into components; populate convenience keys for geocoders that want them | |||||
| 1252 | 69 | 90 | my ($latitude, $longitude) = split(/,/, $latlng); | |||
| 1253 | 69 | 143 | $params->{'lat'} //= $latitude; | |||
| 1254 | 69 | 103 | $params->{'lon'} //= $longitude; | |||
| 1255 | ||||||
| 1256 | # Check L1 / L2 cache before hitting any backend. A list-context call | |||||
| 1257 | # caches every address as an arrayref and a scalar-context call caches | |||||
| 1258 | # one string, so the entry is unpacked for the context asking for it. | |||||
| 1259 | 69 | 74 | if(defined(my $rc = $self->_cache($latlng))) { | |||
| 1260 | 7 | 13 | if(ref($rc) eq 'ARRAY') { | |||
| 1261 | 4 3 | 6 7 | return wantarray ? @{$rc} : $rc->[0]; | |||
| 1262 | } | |||||
| 1263 | 3 | 6 | return $rc; | |||
| 1264 | } | |||||
| 1265 | ||||||
| 1266 | 62 | 61 | my @call_details = caller(0); | |||
| 1267 | ||||||
| 1268 | 62 62 | 416 53 | foreach my $g (@{$self->{geocoders}}) { | |||
| 1269 | 65 | 37 | my $geocoder = $g; | |||
| 1270 | ||||||
| 1271 | # Apply the per-geocoder limit guard for hashref entries | |||||
| 1272 | 65 | 64 | if(ref($geocoder) eq 'HASH') { | |||
| 1273 | 6 | 14 | if(exists($geocoder->{'limit'}) && defined(my $limit = $geocoder->{'limit'})) { | |||
| 1274 | 5 | 9 | $self->_debug("limit: $limit\n"); | |||
| 1275 | 5 | 5 | if($limit <= 0) { | |||
| 1276 | 4 | 5 | next; | |||
| 1277 | } | |||||
| 1278 | 1 | 1 | $geocoder->{'limit'}--; | |||
| 1279 | } | |||||
| 1280 | 2 | 2 | $geocoder = $g->{'geocoder'}; | |||
| 1281 | } | |||||
| 1282 | ||||||
| 1283 | 61 | 100 | $self->_debug('trying ', ref($geocoder), "\n"); | |||
| 1284 | ||||||
| 1285 | 61 | 57 | if(wantarray) { | |||
| 1286 | # ââ List context: collect all address strings from this geocoder âââ | |||||
| 1287 | 22 | 11 | my @rc; | |||
| 1288 | my @locs; | |||||
| 1289 | 22 22 22 | 16 30 33 | eval { @locs = $geocoder->reverse_geocode(%{$params}) }; | |||
| 1290 | ||||||
| 1291 | # Some geocoders (e.g. Geo::Coder::GeoApify) use strict parameter | |||||
| 1292 | # validation and reject the 'latlng' key as unknown. Retry without | |||||
| 1293 | # it -- lat and lon are already in %params from the split above. | |||||
| 1294 | # Other geocoders (e.g. Geo::Coder::CA) require 'latlng', so the | |||||
| 1295 | # first attempt must include it. | |||||
| 1296 | 22 | 104 | if($@ =~ /Unknown parameter.*latlng|latlng.*[Uu]nknown/s) { | |||
| 1297 | 2 2 | 1 4 | my %no_latlng = %{$params}; | |||
| 1298 | 2 | 2 | delete $no_latlng{'latlng'}; | |||
| 1299 | 2 | 2 | $@ = ''; | |||
| 1300 | 2 2 | 2 3 | eval { @locs = $geocoder->reverse_geocode(%no_latlng) }; | |||
| 1301 | } | |||||
| 1302 | ||||||
| 1303 | 22 | 31 | if($@) { | |||
| 1304 | 3 3 | 1 9 | CORE::push @{$self->{'log'}}, { | |||
| 1305 | line => $call_details[2], | |||||
| 1306 | location => $latlng, | |||||
| 1307 | geocoder => ref($geocoder), | |||||
| 1308 | timetaken => 0, | |||||
| 1309 | wantarray => 1, | |||||
| 1310 | error => $@, | |||||
| 1311 | }; | |||||
| 1312 | 3 | 7 | $self->_warn(ref($geocoder), " '$latlng': $@"); | |||
| 1313 | 3 | 959 | next; | |||
| 1314 | } | |||||
| 1315 | ||||||
| 1316 | 19 | 25 | $self->_debug(Data::Dumper->new([\@locs])->Dump()) if($self->{'debug'} >= 2); | |||
| 1317 | ||||||
| 1318 | 19 | 21 | foreach my $loc (@locs) { | |||
| 1319 | 27 | 21 | next unless(defined($loc)); | |||
| 1320 | ||||||
| 1321 | # A plain string is the address itself, as it is in scalar | |||||
| 1322 | # context; anything else that is not a HASH has none to give | |||||
| 1323 | 26 | 26 | if(!ref($loc)) { | |||
| 1324 | 2 | 2 | CORE::push @rc, $loc if(length($loc)); | |||
| 1325 | 2 | 2 | next; | |||
| 1326 | } | |||||
| 1327 | 24 | 40 | next unless(reftype($loc) eq 'HASH'); | |||
| 1328 | ||||||
| 1329 | 23 | 29 | if(my $name = $loc->{'display_name'}) { | |||
| 1330 | # OSM returns the full address in display_name | |||||
| 1331 | 19 | 19 | CORE::push @rc, $name; | |||
| 1332 | } elsif($loc->{'city'}) { | |||||
| 1333 | # Geo::Coder::CA: build the address from individual fields | |||||
| 1334 | 2 | 4 | CORE::push @rc, _build_ca_address($loc); | |||
| 1335 | } elsif($loc->{features}) { | |||||
| 1336 | # GeoApify: formatted string inside a features array | |||||
| 1337 | CORE::push @rc, | |||||
| 1338 | 2 | 3 | $loc->{features}[0]->{properties}{formatted}; | |||
| 1339 | 2 | 2 | last; # only one result from this provider | |||
| 1340 | } | |||||
| 1341 | } | |||||
| 1342 | ||||||
| 1343 | 19 19 | 11 59 | CORE::push @{$self->{'log'}}, { | |||
| 1344 | line => $call_details[2], | |||||
| 1345 | location => $latlng, | |||||
| 1346 | geocoder => ref($geocoder), | |||||
| 1347 | timetaken => 0, | |||||
| 1348 | wantarray => 1, | |||||
| 1349 | result => \@rc, | |||||
| 1350 | }; | |||||
| 1351 | ||||||
| 1352 | # A geocoder with no address for the position has not answered; | |||||
| 1353 | # the next one may | |||||
| 1354 | 19 | 25 | next unless(scalar(@rc)); | |||
| 1355 | ||||||
| 1356 | 17 | 20 | $self->_cache($latlng, \@rc); | |||
| 1357 | 17 | 53 | return @rc; | |||
| 1358 | ||||||
| 1359 | } else { | |||||
| 1360 | # ââ Scalar context: return the first address string ââââââââââââââââ | |||||
| 1361 | my $rc = $self->_cache($latlng) | |||||
| 1362 | 39 39 39 | 40 22 57 | // eval { $geocoder->reverse_geocode(%{$params}) }; | |||
| 1363 | ||||||
| 1364 | # Same strict-validation fallback as the list-context path above | |||||
| 1365 | 39 | 206 | if($@ =~ /Unknown parameter.*latlng|latlng.*[Uu]nknown/s) { | |||
| 1366 | 7 7 | 6 12 | my %no_latlng = %{$params}; | |||
| 1367 | 7 | 8 | delete $no_latlng{'latlng'}; | |||
| 1368 | 7 | 5 | $@ = ''; | |||
| 1369 | 7 7 | 5 33 | $rc = eval { $geocoder->reverse_geocode(%no_latlng) }; | |||
| 1370 | } | |||||
| 1371 | ||||||
| 1372 | 39 | 78 | if($@) { | |||
| 1373 | 9 9 | 7 30 | CORE::push @{$self->{'log'}}, { | |||
| 1374 | line => $call_details[2], | |||||
| 1375 | location => $latlng, | |||||
| 1376 | geocoder => ref($geocoder), | |||||
| 1377 | timetaken => 0, | |||||
| 1378 | wantarray => 0, | |||||
| 1379 | error => $@, | |||||
| 1380 | }; | |||||
| 1381 | 9 | 27 | $self->_warn(ref($geocoder), " '$latlng': $@"); | |||
| 1382 | 9 | 2732 | next; | |||
| 1383 | } | |||||
| 1384 | ||||||
| 1385 | # A bare string needs no further processing | |||||
| 1386 | 30 | 27 | next unless defined $rc; | |||
| 1387 | 28 | 24 | if(!ref($rc)) { | |||
| 1388 | 3 3 | 2 11 | CORE::push @{$self->{'log'}}, { | |||
| 1389 | line => $call_details[2], | |||||
| 1390 | location => $latlng, | |||||
| 1391 | geocoder => ref($geocoder), | |||||
| 1392 | timetaken => 0, | |||||
| 1393 | wantarray => 0, | |||||
| 1394 | result => $rc, | |||||
| 1395 | }; | |||||
| 1396 | 3 | 9 | return $rc; | |||
| 1397 | } | |||||
| 1398 | ||||||
| 1399 | 25 | 32 | $self->_debug(Data::Dumper->new([$rc])->Dump()) if($self->{'debug'} >= 2); | |||
| 1400 | ||||||
| 1401 | 25 | 36 | if(my $name = $rc->{'display_name'}) { | |||
| 1402 | # OSM | |||||
| 1403 | 20 20 | 17 61 | CORE::push @{$self->{'log'}}, { | |||
| 1404 | line => $call_details[2], | |||||
| 1405 | location => $latlng, | |||||
| 1406 | geocoder => ref($geocoder), | |||||
| 1407 | timetaken => 0, | |||||
| 1408 | wantarray => 0, | |||||
| 1409 | result => $name, | |||||
| 1410 | }; | |||||
| 1411 | 20 | 15 | return $self->_cache($latlng, $name); | |||
| 1412 | } | |||||
| 1413 | ||||||
| 1414 | 5 | 7 | if($rc->{'city'}) { | |||
| 1415 | # Geo::Coder::CA | |||||
| 1416 | 3 | 5 | my $name = _build_ca_address($rc); | |||
| 1417 | 3 3 | 3 8 | CORE::push @{$self->{'log'}}, { | |||
| 1418 | line => $call_details[2], | |||||
| 1419 | location => $latlng, | |||||
| 1420 | geocoder => ref($geocoder), | |||||
| 1421 | timetaken => 0, | |||||
| 1422 | wantarray => 0, | |||||
| 1423 | result => $name, | |||||
| 1424 | }; | |||||
| 1425 | 3 | 5 | return $self->_cache($latlng, $name); | |||
| 1426 | } | |||||
| 1427 | ||||||
| 1428 | 2 | 2 | if($rc->{features}) { | |||
| 1429 | # GeoApify | |||||
| 1430 | 2 | 3 | my $name = $rc->{features}[0]->{properties}{formatted}; | |||
| 1431 | 2 2 | 2 6 | CORE::push @{$self->{'log'}}, { | |||
| 1432 | line => $call_details[2], | |||||
| 1433 | location => $latlng, | |||||
| 1434 | geocoder => ref($geocoder), | |||||
| 1435 | timetaken => 0, | |||||
| 1436 | wantarray => 0, | |||||
| 1437 | result => $name, | |||||
| 1438 | }; | |||||
| 1439 | 2 | 2 | return $self->_cache($latlng, $name); | |||
| 1440 | } | |||||
| 1441 | } | |||||
| 1442 | } | |||||
| 1443 | ||||||
| 1444 | 17 | 42 | return; | |||
| 1445 | } | |||||
| 1446 | ||||||
| 1447 | # ============================================================================= | |||||
| 1448 | ||||||
| 1449 - 1511 | =head2 log
Returns the record of what has happened since the object was made, or
since the last C<flush()>. There is one entry for each question asked of
a geocoder, and one for each answer that C<geocode()> serves from the
cache. C<reverse_geocode()> adds no entry when it answers from the
cache.
for my $entry (@{ $list->log() }) {
printf "%s: %.3fs via %s\n",
$entry->{location},
$entry->{timetaken},
$entry->{geocoder};
}
Each entry is a hash reference with these keys:
=over 4
=item * C<location> - the place name or C<latlng> that was asked about
=item * C<geocoder> - the class name of the geocoder, or C<'cache'>
=item * C<timetaken> - how long the geocoder took, in seconds
=item * C<line> - the line of your program that made the call
=item * C<wantarray> - true if the call was made in list context
=item * C<result> - the answer, when there was one; the string
C<'not found'> when the geocoder had none
=item * C<error> - the error, when the geocoder died or returned an
error; this can be an exception object, not only a string
=back
The returned array reference is the object's own record, not a copy.
=head3 API SPECIFICATION
=head4 INPUT
[]
=head4 OUTPUT
{
type => 'arrayref',
}
# Each element:
# {
# line => Int,
# location => Str,
# timetaken => Num,
# geocoder => Str, # class name, or 'cache'
# wantarray => Bool,
# result => HASHREF | ARRAYREF | Str, # on success
# error => Str | OBJECT | HASHREF, # on failure
# }
=cut | |||||
| 1512 | ||||||
| 1513 | sub log { | |||||
| 1514 | 44 | 3945 | my $self = shift; | |||
| 1515 | ||||||
| 1516 | # Guard against the state left by flush(); always return a valid arrayref | |||||
| 1517 | 44 | 99 | return $self->{'log'} // []; | |||
| 1518 | } | |||||
| 1519 | ||||||
| 1520 | # ============================================================================= | |||||
| 1521 | ||||||
| 1522 - 1545 | =head2 flush
Empties the log and returns the object itself, so that it can be chained.
$list->geocode('Paris, France');
my $entries = $list->log();
$list->flush()->geocode('London, UK');
C<flush> does B<not> empty the cache.
=head3 API SPECIFICATION
=head4 INPUT
[]
=head4 OUTPUT
{
type => 'object',
isa => 'Geo::Coder::List',
}
=cut | |||||
| 1546 | ||||||
| 1547 | sub flush { | |||||
| 1548 | 16 | 109 | my $self = shift; | |||
| 1549 | ||||||
| 1550 | # Reset to an empty arrayref so log() always returns a valid reference | |||||
| 1551 | 16 | 18 | $self->{'log'} = []; | |||
| 1552 | ||||||
| 1553 | 16 | 15 | return $self; | |||
| 1554 | } | |||||
| 1555 | ||||||
| 1556 | # ============================================================================= | |||||
| 1557 | # PRIVATE HELPERS | |||||
| 1558 | # ============================================================================= | |||||
| 1559 | ||||||
| 1560 | # _build_ca_address | |||||
| 1561 | # | |||||
| 1562 | # Purpose: Assemble a printable address string from a Geo::Coder::CA | |||||
| 1563 | # reverse-geocode response. The CA response uses different keys | |||||
| 1564 | # for US addresses (nested under 'usa') vs Canadian ones. | |||||
| 1565 | # | |||||
| 1566 | # Entry: $loc - HASHREF from Geo::Coder::CA reverse_geocode() | |||||
| 1567 | # | |||||
| 1568 | # Exit: Returns a plain string, or empty string if nothing was found. | |||||
| 1569 | # | |||||
| 1570 | # Notes: Street number and name are joined with a space; other parts | |||||
| 1571 | # (city, province/state, country) are joined with ', '. | |||||
| 1572 | ||||||
| 1573 | sub _build_ca_address | |||||
| 1574 | { | |||||
| 1575 | 16 | 16892 | my $loc = $_[0]; | |||
| 1576 | 16 | 12 | my $name = ''; | |||
| 1577 | ||||||
| 1578 | 16 | 20 | if(my $usa = $loc->{'usa'}) { | |||
| 1579 | # US address layout inside a CA result | |||||
| 1580 | 5 | 9 | $name = $usa->{'usstnumber'} // ''; | |||
| 1581 | # Street name follows number with a space; if no number, no leading space | |||||
| 1582 | 5 | 10 | $name .= ($name ? ' ' : '') . $usa->{'usstaddress'} if $usa->{'usstaddress'}; | |||
| 1583 | # City, state, country each separated by ', '; skip separator if name empty | |||||
| 1584 | 5 | 12 | $name .= ($name ? ', ' : '') . $usa->{'uscity'} if $usa->{'uscity'}; | |||
| 1585 | 5 | 7 | $name .= ($name ? ', ' : '') . $usa->{'state'} if $usa->{'state'}; | |||
| 1586 | # Country is always appended for the US branch | |||||
| 1587 | 5 | 6 | $name .= ($name ? ', ' : '') . 'USA'; | |||
| 1588 | } else { | |||||
| 1589 | # Canadian address layout | |||||
| 1590 | 11 | 23 | $name = $loc->{'stnumber'} // ''; | |||
| 1591 | # Street name follows number with a space; if no number, no leading space | |||||
| 1592 | 11 | 22 | $name .= ($name ? ' ' : '') . $loc->{'staddress'} if $loc->{'staddress'}; | |||
| 1593 | 11 | 21 | $name .= ($name ? ', ' : '') . $loc->{'city'} if $loc->{'city'}; | |||
| 1594 | 11 | 20 | $name .= ($name ? ', ' : '') . $loc->{'prov'} if $loc->{'prov'}; | |||
| 1595 | } | |||||
| 1596 | ||||||
| 1597 | 16 | 26 | return $name; | |||
| 1598 | } | |||||
| 1599 | ||||||
| 1600 | # ----------------------------------------------------------------------------- | |||||
| 1601 | ||||||
| 1602 | # _cache | |||||
| 1603 | # | |||||
| 1604 | # Read from or write to the two-level cache. | |||||
| 1605 | # L1 is an in-process HASH (always active). | |||||
| 1606 | # L2 is an optional CHI-compatible object or a plain HASH ref. | |||||
| 1607 | # | |||||
| 1608 | # Entry (write): _cache($key, $value) | |||||
| 1609 | # $value may be undef, which is stored as $NOT_FOUND_SENTINEL so | |||||
| 1610 | # subsequent reads can distinguish "cached not-found" from "never | |||||
| 1611 | # looked up". Detect the write path by testing scalar(@_) before | |||||
| 1612 | # shifting, not by truthiness of the value. | |||||
| 1613 | # | |||||
| 1614 | # Entry (read): _cache($key) | |||||
| 1615 | # | |||||
| 1616 | # Exit (write): Returns $value (undef if the location was not found). | |||||
| 1617 | # Exit (read): Returns the cached value, or undef if not in cache. | |||||
| 1618 | # $NOT_FOUND_SENTINEL is never surfaced to callers; undef is | |||||
| 1619 | # returned in its place so callers handle both cases identically. | |||||
| 1620 | # | |||||
| 1621 | # Side effects: May update $self->{locations} (L1) and $self->{'cache'} (L2). | |||||
| 1622 | # | |||||
| 1623 | # Notes: Cache TTLs are taken from $self->{cache_*_duration}, which are | |||||
| 1624 | # initialised from %config and overridable via Object::Configure. | |||||
| 1625 | # Not-found sentinels are stored only in L1 to avoid leaking | |||||
| 1626 | # internal implementation details into an external L2 store. | |||||
| 1627 | ||||||
| 1628 | sub _cache { | |||||
| 1629 | 602 | 906 | my $self = shift; | |||
| 1630 | 602 | 289 | my $key = shift; | |||
| 1631 | ||||||
| 1632 | # ââ Write path ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ | |||||
| 1633 | # Detect a write call by the presence of a third argument (even if undef). | |||||
| 1634 | # Testing truthiness of the value would silently swallow not-found results. | |||||
| 1635 | ||||||
| 1636 | 602 | 451 | if(scalar(@_)) { | |||
| 1637 | 261 | 121 | my $value = shift; | |||
| 1638 | ||||||
| 1639 | # Store a sentinel for not-found so we can skip backends on repeat calls | |||||
| 1640 | 261 | 189 | my $stored = defined($value) ? $value : $NOT_FOUND_SENTINEL; | |||
| 1641 | 261 | 288 | $self->{locations}->{$key} = $stored; | |||
| 1642 | ||||||
| 1643 | 261 | 122 | my $rc = $value; | |||
| 1644 | ||||||
| 1645 | 261 | 251 | if($self->{'cache'}) { | |||
| 1646 | # L2 is written from copies: the caller is returned $value and L1 | |||||
| 1647 | # holds it, so what is stripped for storage must not be stripped | |||||
| 1648 | # from what they see | |||||
| 1649 | 35 | 22 | my $l2value = $value; | |||
| 1650 | 35 | 18 | my $duration; | |||
| 1651 | ||||||
| 1652 | 35 | 66 | if(ref($value) eq 'ARRAY') { | |||
| 1653 | 10 11 10 | 6 12 8 | $l2value = [ map { _l2_copy($_, $self->{'debug'}) } @{$value} ]; | |||
| 1654 | ||||||
| 1655 | 10 10 | 23 10 | foreach my $item (@{$l2value}) { | |||
| 1656 | 11 | 10 | next unless ref($item) eq 'HASH'; | |||
| 1657 | ||||||
| 1658 | 9 | 9 | unless(_has_lat($item)) { | |||
| 1659 | # Geometry without a lat may be a transient failure, so | |||||
| 1660 | # retry soon; no geometry at all is a miss | |||||
| 1661 | $duration //= defined($item->{geometry}) | |||||
| 1662 | ? $self->{'cache_part_duration'} | |||||
| 1663 | 5 | 12 | : $self->{'cache_miss_duration'}; | |||
| 1664 | 5 | 4 | $rc = undef; | |||
| 1665 | } | |||||
| 1666 | } | |||||
| 1667 | ||||||
| 1668 | # All items were clean: use the full hit duration | |||||
| 1669 | 10 | 16 | $duration //= $self->{'cache_hit_duration'}; | |||
| 1670 | ||||||
| 1671 | } elsif(ref($value) eq 'HASH') { | |||||
| 1672 | 21 | 31 | $l2value = _l2_copy($value, $self->{'debug'}); | |||
| 1673 | ||||||
| 1674 | 21 | 29 | if(_has_lat($l2value)) { | |||
| 1675 | # Confirmed location: cache for a full month | |||||
| 1676 | 14 | 18 | $duration = $self->{'cache_hit_duration'}; | |||
| 1677 | } elsif(defined($l2value->{geometry})) { | |||||
| 1678 | # Partial geometry: may be a transient failure, retry soon | |||||
| 1679 | 4 | 3 | $duration = $self->{'cache_part_duration'}; | |||
| 1680 | 4 | 5 | $rc = undef; | |||
| 1681 | } else { | |||||
| 1682 | # No geometry at all: place probably does not exist | |||||
| 1683 | 3 | 2 | $duration = $self->{'cache_miss_duration'}; | |||
| 1684 | 3 | 3 | $rc = undef; | |||
| 1685 | } | |||||
| 1686 | } else { | |||||
| 1687 | # Scalar string or a blessed object (e.g. Geo::Location::Point) | |||||
| 1688 | 4 | 7 | $l2value = _l2_copy($value, 1); | |||
| 1689 | 4 | 4 | $duration = $self->{'cache_hit_duration'}; | |||
| 1690 | } | |||||
| 1691 | ||||||
| 1692 | 35 | 47 | $self->_debug(Data::Dumper->new([$l2value])->Dump()) if($self->{'debug'}); | |||
| 1693 | ||||||
| 1694 | # Do not push the not-found sentinel into L2 | |||||
| 1695 | 35 | 71 | if(!defined($value)) { | |||
| 1696 | # value is not-found; L1 sentinel is sufficient | |||||
| 1697 | } elsif(ref($self->{'cache'}) eq 'HASH') { | |||||
| 1698 | 15 | 18 | $self->{'cache'}->{$key} = $l2value; | |||
| 1699 | } else { | |||||
| 1700 | # L2 is only an optimisation: a store that is down must not | |||||
| 1701 | # lose the caller the result that has already been found | |||||
| 1702 | 16 | 9 | local $@; | |||
| 1703 | 16 16 15 | 14 27 80 | eval { $self->{'cache'}->set($key, $l2value, $duration); 1 } | |||
| 1704 | or $self->_warn(__PACKAGE__, ": cache set failed for '$key': $@"); | |||||
| 1705 | } | |||||
| 1706 | } | |||||
| 1707 | ||||||
| 1708 | 261 | 514 | return $rc; | |||
| 1709 | } | |||||
| 1710 | ||||||
| 1711 | # ââ Read path âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ | |||||
| 1712 | ||||||
| 1713 | # Check L1 first (in-process, no serialisation cost) | |||||
| 1714 | 341 | 232 | my $rc = $self->{'locations'}->{$key}; | |||
| 1715 | ||||||
| 1716 | # Fall through to L2 only when L1 has no entry for this key | |||||
| 1717 | 341 | 513 | if((!defined($rc)) && $self->{'cache'}) { | |||
| 1718 | 12 | 33 | if(ref($self->{'cache'}) eq 'HASH') { | |||
| 1719 | 8 | 10 | $rc = $self->{'cache'}->{$key}; | |||
| 1720 | } else { | |||||
| 1721 | # A store that cannot be read is treated as a miss, so the lookup | |||||
| 1722 | # goes to the geocoders rather than failing | |||||
| 1723 | 4 | 5 | local $@; | |||
| 1724 | 4 4 | 3 7 | $rc = eval { $self->{'cache'}->get($key) }; | |||
| 1725 | 4 | 17 | $self->_warn(__PACKAGE__, ": cache get failed for '$key': $@") if($@); | |||
| 1726 | } | |||||
| 1727 | } | |||||
| 1728 | ||||||
| 1729 | 341 | 665 | return unless defined $rc; | |||
| 1730 | ||||||
| 1731 | # Translate the not-found sentinel back to undef for the caller | |||||
| 1732 | 52 | 112 | return if ref($rc) && (ref($rc) eq _NOT_FOUND_CLASS); | |||
| 1733 | ||||||
| 1734 | # Restore the convenience aliases that were stripped before L2 storage | |||||
| 1735 | 43 | 47 | if(ref($rc) eq 'HASH') { | |||
| 1736 | 29 | 33 | return unless defined($rc->{geometry}{location}{lat}); | |||
| 1737 | 27 | 42 | $rc->{'lat'} //= $rc->{geometry}{location}{lat}; | |||
| 1738 | 27 | 28 | $rc->{'lng'} //= $rc->{geometry}{location}{lng}; | |||
| 1739 | 27 | 25 | $rc->{'lon'} //= $rc->{geometry}{location}{lng}; | |||
| 1740 | } | |||||
| 1741 | ||||||
| 1742 | 41 | 37 | return $rc; | |||
| 1743 | } | |||||
| 1744 | ||||||
| 1745 | # A copy of one result fit for L2. The geocoder object becomes its class name, | |||||
| 1746 | # since a handle it holds may not serialise, and unless $keep_all only geometry | |||||
| 1747 | # is kept, to keep the entry small. Anything that is not a HASH-based | |||||
| 1748 | # reference is returned as it is. | |||||
| 1749 | sub _l2_copy { | |||||
| 1750 | 42 | 4580 | my ($item, $keep_all) = @_; | |||
| 1751 | ||||||
| 1752 | 42 | 113 | return $item unless ref($item) && (reftype($item) eq 'HASH'); | |||
| 1753 | ||||||
| 1754 | 34 34 | 21 61 | my $copy = { %{$item} }; | |||
| 1755 | 34 | 65 | $copy = bless $copy, ref($item) if blessed($item); | |||
| 1756 | ||||||
| 1757 | 34 | 52 | $copy->{'geocoder'} = ref($copy->{'geocoder'}) if ref($copy->{'geocoder'}); | |||
| 1758 | ||||||
| 1759 | # A blessed object keeps its fields: its accessors may rely on them | |||||
| 1760 | 34 | 70 | unless($keep_all || blessed($item)) { | |||
| 1761 | 28 28 78 28 | 24 22 49 23 | delete @{$copy}{grep { $_ ne 'geometry' } keys %{$copy}}; | |||
| 1762 | } | |||||
| 1763 | ||||||
| 1764 | 34 | 38 | return $copy; | |||
| 1765 | } | |||||
| 1766 | ||||||
| 1767 | # Whether a result carries a latitude, looked for without auto-vivifying the | |||||
| 1768 | # path to it in a structure that may be shared with the caller | |||||
| 1769 | sub _has_lat { | |||||
| 1770 | 36 | 5476 | my $item = shift; | |||
| 1771 | ||||||
| 1772 | 36 | 55 | return 0 unless ref($item->{geometry}) eq 'HASH'; | |||
| 1773 | 28 | 46 | return 0 unless ref($item->{geometry}{location}) eq 'HASH'; | |||
| 1774 | 21 | 44 | return defined($item->{geometry}{location}{lat}) ? 1 : 0; | |||
| 1775 | } | |||||
| 1776 | ||||||
| 1777 | # Emit a debug message somewhere | |||||
| 1778 | sub _debug { | |||||
| 1779 | 845 | 1423 | my $self = shift; | |||
| 1780 | ||||||
| 1781 | # Callers may end the message with a newline or not; it is printed with | |||||
| 1782 | # exactly one, and given to the logger with none | |||||
| 1783 | 845 2324 | 509 2578 | my $message = join('', map { $_ // '' } @_); | |||
| 1784 | 845 | 1152 | $message .= "\n" unless($message =~ /\n\z/); | |||
| 1785 | ||||||
| 1786 | 845 | 637 | if(my $logger = $self->{logger}) { | |||
| 1787 | 825 | 1109 | (my $line = $message) =~ s/\n\z//; | |||
| 1788 | 825 | 775 | $logger->debug($line); | |||
| 1789 | } | |||||
| 1790 | 845 | 18366 | if($self->{debug}) { | |||
| 1791 | 25 | 64 | print $message; | |||
| 1792 | } | |||||
| 1793 | } | |||||
| 1794 | ||||||
| 1795 | # Emit a warning message somewhere | |||||
| 1796 | sub _warn { | |||||
| 1797 | 34 | 126 | my $self = shift; | |||
| 1798 | ||||||
| 1799 | 34 | 45 | if(my $logger = $self->{logger}) { | |||
| 1800 | 28 | 43 | $logger->warn(@_); | |||
| 1801 | } else { | |||||
| 1802 | 6 | 101 | Carp::carp(@_); | |||
| 1803 | } | |||||
| 1804 | } | |||||
| 1805 | ||||||
| 1806 | # Emit an error message somewhere | |||||
| 1807 | sub _error { | |||||
| 1808 | 20 | 91 | my $self = shift; | |||
| 1809 | ||||||
| 1810 | 20 | 24 | if(my $logger = $self->{logger}) { | |||
| 1811 | 19 | 34 | $logger->error(@_); | |||
| 1812 | 19 | 7136 | die @_; | |||
| 1813 | } else { | |||||
| 1814 | 1 | 10 | Carp::croak(@_); | |||
| 1815 | } | |||||
| 1816 | } | |||||
| 1817 | ||||||
| 1818 | # ============================================================================= | |||||
| 1819 | # DOCUMENTATION | |||||
| 1820 | # ============================================================================= | |||||
| 1821 | ||||||
| 1822 - 2288 | =head1 COMMON PITFALLS
=over 4
=item * B<Scalar context and list context give different answers.>
C<geocode()> and C<reverse_geocode()> return one answer in scalar
context and all answers in list context. Perl decides the context from
the code around the call, and some places give list context when you may
not expect it:
my %row = (name => $name, place => $list->geocode($name)); # list context!
print 'Found: ', $list->geocode($name); # list context!
return $list->geocode($name); # your caller decides
When nothing is found, list context returns an I<empty list>, so in the
first example the keys and values of C<%row> move out of step. Use
C<scalar()> to be sure:
my %row = (name => $name, place => scalar($list->geocode($name)));
=item * B<C<undef> means "not found" and also "bad empty input".>
C<geocode()> returns C<undef> both when no geocoder knows the place and
when C<location> is C<undef> or empty (it also warns in that case). Other
bad input, such as a reference or a string of digits, does not return
C<undef>: it croaks. Wrap the call in C<eval> when the text comes from
outside your program.
=item * B<"Not found" is remembered for the life of the object.>
When no geocoder finds a place, L1 remembers that, and later calls for
the same place return C<undef> at once without asking any geocoder, even
if a geocoder was only down for a moment. C<flush()> does not forget it,
and nor does a clone, which shares L1. Make a new object with
C<Geo::Coder::List-E<gt>new()> to ask again. L2 never stores "not
found".
=item * B<An answer from L2 has only its position.>
To keep L2 small, only C<geometry> is stored there (unless C<debug> is
set). An answer that comes from L2, for example in a new run of your
program, has C<geometry>, C<lat>, C<lng>, C<lon> and C<geocoder> (set to
C<'cache'>), but none of the provider's other fields, such as an address
or a place type. Answers from L1 keep every field.
=item * B<The C<geocoder> key is not always an object.>
It is the geocoder object when a geocoder answered, and the string
C<'cache'> when the cache answered. Do not call methods on it without
checking. To get a printable name in both cases:
my $source = ref($result->{geocoder}) || $result->{geocoder};
=item * B<Changing an answer changes the cache.>
The first answer for a place is the same hash that L1 stores. If you
change it, later answers for that place, which are copies of the L1 entry,
include your change. Copy the answer before you change it.
=item * B<The hash you give to C<push()> is changed.>
C<limit> counts down inside the hash reference that you pushed. If you
push the same hash reference into two lists, they share one limit. A
clone made with C<< $list->new() >> has its own copy.
=item * B<The cache key is the exact text.>
C<'Paris, France'>, C<'paris, france'> and C<'Paris,France'> are three
different keys, and each one asks the geocoders again. Only runs of
white space are made into one space before the text is used as a key.
=item * B<HTML entities are always decoded.>
C<'AT&T Park'> is looked up as C<'AT&T Park'>. There is no way to
look up text that really contains C<&>.
=item * B<Strings of digits depend on an optional module.>
When L<Geo::Location::Predicate> is installed, C<geocode('12345')> croaks.
When it is not installed, C<'12345'> is sent to the geocoders. If you
handle postcodes or ZIP codes, add the country, such as
C<'12345, USA'>.
=item * B<C<reverse_geocode()> ignores C<regex>.>
A geocoder added with C<regex> is still asked to reverse geocode every
position. Only C<limit> is honoured.
=item * B<The log grows until you empty it.>
Every call adds to the log. In a long-running program, such as a web
server, call C<flush()> from time to time.
=item * B<Debug output goes to standard output.>
With C<debug> set, messages are printed to C<STDOUT>, not C<STDERR>, so
they mix with your program's own output. The same messages are always
given to the configured C<logger> at debug level, whether C<debug> is set
or not, so a logger is the way to collect them somewhere else.
=back
=head1 ENCODING
=over 4
=item * B<Place names> (C<location> in C<geocode()>) may contain any
Unicode text, including accented letters, other alphabets and emoji.
Give them to C<geocode()> as Perl I<character> strings: if your text is
UTF-8 bytes from a file or a web form, decode it first, for example with
C<Encode::decode('UTF-8', $bytes)>. HTML entities in the text are decoded
into characters, so text that mixes UTF-8 bytes that are not yet decoded
with entities becomes garbled. The text is passed to each geocoder unchanged apart from
that; whether a geocoder understands emoji is up to the geocoder.
=item * B<White space.> Runs of ASCII spaces, tabs and new lines are
always made into one space. Unicode spaces, such as the no-break space,
may or may not be, depending on how Perl stores the string; do not rely on
it.
=item * B<Positions> (C<latlng> in C<reverse_geocode()>) must be ASCII.
Only the digits C<0> to C<9> are accepted. Other Unicode digits, such as
Eastern Arabic or full-width digits, are refused.
=item * B<Cache keys> are the cleaned text, so they can contain any
character. L1 and a plain-hash L2 accept them all. An L2 object must
accept character-string keys; L<CHI> does.
=item * B<Answers> are returned as the geocoder gave them. Most
geocoders return character strings.
=item * B<Debug output> prints place names to C<STDOUT>. If they contain
characters above 255, Perl warns C<Wide character in print> unless
C<STDOUT> has an encoding layer, such as
C<binmode(STDOUT, ':encoding(UTF-8)')>.
=back
=head1 AUTHOR
Nigel Horne, C<< <njh at nigelhorne.com> >>
=head1 BUGS
Please report any bugs or feature requests to
C<bug-geo-coder-list at rt.cpan.org>, or through the web interface at
L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Geo-Coder-List>.
Known limitations:
=over 4
=item * C<reverse_geocode()> does not yet support L<Geo::Location::Point> objects.
=item * C<reverse_geocode()> does not honour C<regex>.
=back
=head1 SEE ALSO
=over 4
=item * L<Test Dashboard|https://nigelhorne.github.io/Geo-Coder-List/coverage/>
=item * L<CHI>
=item * L<Geo::Coder::All>
=item * L<Geo::Coder::GooglePlaces>
=item * L<Geo::Coder::Many>
=item * L<Geo::Location::Predicate>
=item * L<HTML::GoogleMaps::V3>
=item * L<HTML::OSM>
=item * L<Configure an Object at Runtime|Object::Configure>
=item * L<Readonly>
=back
=head1 SUPPORT
You can find documentation for this module with the perldoc command:
perldoc Geo::Coder::List
=over 4
=item * RT: CPAN's request tracker
L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Geo-Coder-List>
=item * MetaCPAN
L<https://metacpan.org/release/Geo-Coder-List>
=back
=encoding utf-8
=head1 FORMAL SPECIFICATION
The specifications below use the Z notation. They describe what each
method does to the state of the object; they do not describe how.
=head2 State
[Geocoder, Regex, Text, Result, LogEntry, Duration]
Key == Text
NotFound ::= notFound
Entry ::= plainâ¨â¨Geocoderâ©â©
| routedâ¨â¨Geocoder à â Text à (⤠⪠{â})â©â©
-- the geocoder, the texts its regex matches, its limit
ââ GeoCoderList âââââââââââââââââââââââââââââââââââââââââââââââââ
â geocoders : seq Entry
â L1 : Key ⸠(Result ⪠NotFound)
â L2 : Key ⸠Result
â log : seq LogEntry
â debug : 0 ⥠2
â hit, part, miss : Duration
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â notFound â ran L2
â â r : ran L2 ⢠dom r â {geometry, geocoder} ⨠debug > 0
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
usable : â Result
usable = { r : Result | r.geometry.location.lat â â¥
â§ r.geometry.location.lng â ⥠}
clean : Text â Text
-- runs of white space become one space; HTML entities are decoded
isPlace : (Text ⪠Reference) â ð¹
-- Geo::Location::Predicate::_valid_location when installed;
-- otherwise true for every non-empty Text and false for a Reference
=head2 new
ââ New ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â GeoCoderListâ²
â args? : Key ⸠Value
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â geocodersâ² = args?(geocoders) if geocoders â dom args?, else â¨â©
â L1â² = â
â logâ² = â¨â©
â L2â² = args?(cache) if cache â dom args?, else â
â hitâ² = 1 month â§ partâ² = 1 day â§ missâ² = 1 week -- unless given
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ââ Clone ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â GeoCoderList ; GeoCoderListâ²
â args? : Key ⸠Value
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â geocodersâ² = geocoders -- equal values, distinct copies
â L1â² = L1 â§ L2â² = L2 -- the same stores, shared
â logâ² = â¨â©
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
=head2 push
ââ Push âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ÎGeoCoderList
â g? : Entry
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â g? â â¥
â geocodersâ² = geocoders â â¨g?â©
â L1â² = L1 â§ L2â² = L2 â§ logâ² = log
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
PushUndef â [ ÎGeoCoderList ; g? : Entry | g? = ⥠] â croak
=head2 geocode
asks : Entry à Text ⸠ð¹
asks(plain g, t) = true
asks(routed(g, M, n), t) = t â M â§ n > 0
spend : Entry à Text â Entry
spend(routed(g, M, n), t) = routed(g, M, n â 1) if t â M â§ n > 0
spend(e, t) = e otherwise
ââ Geocode ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ÎGeoCoderList
â loc? : Text ⪠Reference
â result! : Result ⪠{â¥}
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â let t == clean(loc?) â¢
â ¬ isPlace(t) â croak
â
â t â dom L1 â§ L1(t) â Result
â â result! = L1(t) â {geocoder ⦠cache}
â â§ geocodersâ² = geocoders â§ L1â² = L1
â
â t â dom L1 â§ L1(t) = notFound
â â result! = ⥠⧠geocodersâ² = geocoders â§ L1â² = L1
â
â t â dom L1
â â (â i : dom geocoders â¢
â asks(geocoders i, t)
â â§ (â j : 1 ⥠i â 1 ⢠¬ asks(geocoders j, t)
â ⨠answer(geocoders j, t) â usable)
â â§ answer(geocoders i, t) â usable
â â§ result! = answer(geocoders i, t)
â â§ L1â² = L1 â {t ⦠result!}
â â§ L2â² = L2 â {t ⦠result! â¾ {geometry, geocoder}})
â ⨠(result! = ⥠⧠L1â² = L1 â {t ⦠notFound} â§ L2â² = L2)
â
â geocodersâ² = ⨠e : geocoders ⢠spend(e, t) â© -- for those asked
â logâ² = log â ⨠one entry per geocoder asked, or one for the cache â©
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
=head2 ua
ââ UA âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ÎGeoCoderList
â ua? : UserAgent ⪠{â¥}
â ua! : UserAgent ⪠{â¥}
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ua? = ⥠â ua! = â¥
â ua? â ⥠â ua! = ua?
â â§ (â e : ran geocoders ⢠geocoderOf(e) â ⥠⨠croak)
â â§ (â e : ran geocoders â¢
â geocoderOf(e).uaâ² = clone(ua?) if ua? can clone and agent
â geocoderOf(e).uaâ² = ua? otherwise)
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
=head2 reverse_geocode
LatLng == { s : Text | s â ⦠^W*[-+]?D W*,W*[-+]?D W*$ â§ }
-- D is a decimal number written in the ASCII digits 0-9;
-- W is a space or a tab
ââ ReverseGeocode âââââââââââââââââââââââââââââââââââââââââââââââ
â ÎGeoCoderList
â latlng? : Text
â address! : Text ⪠{â¥}
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â latlng? â LatLng â croak
â
â latlng? â dom L1 â address! = first(L1(latlng?)) â§ L1â² = L1
â
â latlng? â dom L1
â â (â i : dom geocoders â¢
â limitOf(geocoders i) > 0
â â§ addresses(geocoders i, latlng?) â â¨â©
â â§ address! = head addresses(geocoders i, latlng?)
â â§ L1â² = L1 â {latlng? ⦠address!})
â ⨠(address! = ⥠⧠L1â² = L1)
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
=head2 log
ââ Log ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ÎGeoCoderList
â entries! : seq LogEntry
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â entries! = log
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
=head2 flush
ââ Flush ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ÎGeoCoderList
â self! : GeoCoderList
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â logâ² = â¨â©
â geocodersâ² = geocoders â§ L1â² = L1 â§ L2â² = L2
â self! = θGeoCoderListâ²
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
=head1 STATE DIAGRAM
An object has three parts that change over time: its chain of geocoders,
its cache (one state for each place name), and its log. Each part is
shown on its own below.
=head2 The object and its chain
+-------------+ push($g) +--------------+ push($g)
| EMPTY |----------------->| READY |<-----------+
| no geocoder | adds $g to the | 1+ geocoders |------------+
+-------------+ end of chain +--------------+ adds $g to the end
| |
| geocode($t) | new() on the object
| reverse_geocode($p) | clone: copies the chain and
v | limits, shares L1 and L2,
returns undef / () | empties the log
nothing is asked, but a v
geocode() miss is still +--------------+
remembered in L1 | READY (clone)|
+--------------+
=head2 The cache, for one place name $t
+-----------+ geocode($t): a geocoder gives a usable answer
| |----------------------------------------------+
| UNKNOWN | - answer stored in L1 |
| not in L1 | - copy with only geometry stored in L2, |
| | kept for cache_hit_duration (1 month) |
+-----------+ - log gets one entry per geocoder asked |
| ^ v
| | +-----------+
| | L2 entry expires, and a new object | FOUND |<--+
| | is used (L1 dies with its object) | L1 holds | | geocode($t):
| +---------------------------------------------| an answer |---+ no geocoder
| +-----------+ asked; a copy
| geocode($t): no geocoder gives a usable answer returned with
| - L1 stores "not found"; L2 is not written geocoder='cache';
| - log gets one entry per geocoder asked log gets one
v cache entry
+-----------+
| NOT FOUND |<--+ geocode($t): nothing asked, undef / () returned,
| in L1 |---+ nothing logged
+-----------+
|
+-- leaves this state only when the object is destroyed;
flush() and clones do not leave it
Any state: geocode($t) with a reference, digits (when
Geo::Location::Predicate is installed) or entity-encoded digits
--> croaks; the state does not change.
=head2 The limit of one geocoder pushed with limit => N
+--------------+ geocode($t), $t matches regex +--------------+
| ACTIVE |-------------------------------->| ACTIVE |
| limit = n | geocoder asked; limit = n - 1 | limit = n-1 |
+--------------+ +--------------+
| ^ |
| | geocode($t), $t does not match regex: | ... when the
| | geocoder not asked, limit unchanged | limit reaches 0
+------+ v
+--------------+
| EXHAUSTED |<--+
| limit <= 0 |---+
+--------------+ geocode() and
reverse_geocode():
skipped, never asked
=head2 The log
+-----------+ geocode() or reverse_geocode() +-------------+
| EMPTY |---------------------------------->| HAS |<--+
| log = [] | appends one entry per geocoder | ENTRIES |---+ geocode() /
+-----------+ asked, or one for a cache hit +-------------+ reverse_geocode():
^ | appends entries
| flush(): log = [], returns $self |
+-------------------------------------------------+
^
| new() and clone: start EMPTY
=head1 LICENSE AND COPYRIGHT
Copyright 2016-2026 Nigel Horne.
Usage is subject to the GPL2 licence terms.
If you use it,
please let me know.
=cut | |||||
| 2289 | ||||||
| 2290 | 1; | |||||