| File: | blib/lib/Geo/Coder/Free.pm |
| Coverage: | 83.7% |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | package Geo::Coder::Free; | ||||||
| 2 | |||||||
| 3 | 16 16 16 | 1774030 13 210 | use strict; | ||||
| 4 | 16 16 16 | 23 11 330 | use warnings; | ||||
| 5 | 16 16 16 | 1564 53087 30 | use autodie qw(:all); | ||||
| 6 | |||||||
| 7 | 16 16 16 | 79107 16 389 | use Carp; | ||||
| 8 | 16 16 16 | 3305 27 293 | use Geo::Coder::Free::Local; | ||||
| 9 | 16 16 16 | 40 12 156 | use Geo::Coder::Free::MissCache; | ||||
| 10 | 16 16 16 | 3181 24 390 | use Geo::Coder::Free::MaxMind; | ||||
| 11 | 16 16 16 | 2977 22 394 | use Geo::Coder::Free::OpenAddresses; | ||||
| 12 | 16 16 16 | 38 9 361 | use Geo::Coder::Free::Utils qw(_abbreviate _normalize); | ||||
| 13 | 16 16 16 | 25 11 223 | use Geo::Location::Predicate qw(_valid_location); | ||||
| 14 | 16 16 16 | 21 10 120 | use Object::Configure; | ||||
| 15 | 16 16 16 | 22 7 154 | use Params::Get; | ||||
| 16 | 16 16 16 | 21 8 216 | use Readonly; | ||||
| 17 | 16 16 16 | 21 11 265 | use Scalar::Util; | ||||
| 18 | # Imported functions are not methods: without this, $obj->croak() and the like | ||||||
| 19 | # would resolve. The three kept are reached as | ||||||
| 20 | # Geo::Coder::Free::<name> by t/function.t, t/path.t and t/edge_cases.t. | ||||||
| 21 | 16 16 16 | 22 10 30 | use namespace::clean -except => [qw(_valid_location _normalize _abbreviate)]; | ||||
| 22 | |||||||
| 23 | # TODO: from the 0.50 gap analysis | ||||||
| 24 | # | ||||||
| 25 | # Errors and housekeeping: | ||||||
| 26 | # TODO: OpenAddresses.pm croaks with the raw address as its whole message | ||||||
| 27 | # (Carp::croak($city), "TODO: do something here"), and croaks when | ||||||
| 28 | # Geo::StreetAddress::US finds no state. Neither is documented or goes | ||||||
| 29 | # through a message table; return undef instead, or document them | ||||||
| 30 | # TODO: MaxMind->new() croaks ": directory ... doesn't exist" with no package | ||||||
| 31 | # name, because ref($class) is '' when called on the class | ||||||
| 32 | # TODO: xt/mutant_*.t stubs are committed by CI with 117 TODOs, against | ||||||
| 33 | # their own "DO NOT COMMIT" header; complete them or stop committing them | ||||||
| 34 | # | ||||||
| 35 | # Coverage and testing: | ||||||
| 36 | # TODO: ship a small fixture openaddresses.sql and cities.sql so t/openaddr.t | ||||||
| 37 | # and t/maxmind.t run in CI. CI branch coverage is 10.6% for | ||||||
| 38 | # OpenAddresses.pm and 26.9% for MaxMind.pm because neither database is there | ||||||
| 39 | # TODO: accuracy tests - a table of well-known cities checked to within a | ||||||
| 40 | # tolerance with Utils::distance(), which would have caught the | ||||||
| 41 | # "City, Country" bug below | ||||||
| 42 | # TODO: make sure .github/workflows/bare.yml (optional modules hidden) runs on | ||||||
| 43 | # every push; it is the only job that catches optional-module crashes such | ||||||
| 44 | # as 0.49's Geo::Address::Parser one | ||||||
| 45 | # | ||||||
| 46 | # Accuracy: | ||||||
| 47 | # TODO: two-part "City, Country" in MaxMind.pm answers with the first row in a | ||||||
| 48 | # region ("Berlin, Germany" and "Munich, Germany" both give 52.08,7.38; | ||||||
| 49 | # "Toronto, Canada" lands in British Columbia; "Paris, France" is not | ||||||
| 50 | # found). Choose the most populous matching city in the country - cities.sql | ||||||
| 51 | # has Population - which also covers the TODO about returning the central | ||||||
| 52 | # location in MaxMind.pm's geocode POD | ||||||
| 53 | # TODO: fold accents (e.g. Text::Unidecode) as well as matching exactly, so | ||||||
| 54 | # "Zurich" finds the accented spelling in the data | ||||||
| 55 | # | ||||||
| 56 | # Features: | ||||||
| 57 | # TODO: return ranked candidates with confidence in list context, rather than | ||||||
| 58 | # one point (the "Portlands" TODO in OpenAddresses.pm's geocode POD) | ||||||
| 59 | # TODO: move %known_locations (MaxMind.pm, OpenAddresses.pm) and Local.pm's | ||||||
| 60 | # %alternatives into data files, and add the "dbname" parameter to | ||||||
| 61 | # OpenAddresses->new() that its BUGS section asks for | ||||||
| 62 | # TODO: Canadian and Australian address parsing (Local.pm), and non-US parsing | ||||||
| 63 | # in OpenAddresses.pm, for users without Geo::libpostal | ||||||
| 64 | # TODO: JSON/XML output from the website (Display/query.pm) | ||||||
| 65 | # | ||||||
| 66 | # Technical debt: | ||||||
| 67 | # TODO: break OpenAddresses::geocode() (about 900 lines, US parsing nested | ||||||
| 68 | # eight deep) into per-country handlers that can be tested on their own | ||||||
| 69 | # TODO: Free.pm, Local.pm and MaxMind.pm new() "only work with no arguments" | ||||||
| 70 | # when called as a function (FIXME in each) | ||||||
| 71 | # TODO: resolve the TODO/FIXME markers in the VWF-owned Allow.pm, Display.pm, | ||||||
| 72 | # Utils.pm and Config.pm, in ../vwf, and sync | ||||||
| 73 | |||||||
| 74 - 82 | =head1 NAME Geo::Coder::Free - Geocoding using free, locally-hosted databases =head1 VERSION Version 0.51 =cut | ||||||
| 83 | |||||||
| 84 | our $VERSION = '0.51'; | ||||||
| 85 | |||||||
| 86 | =encoding utf-8 | ||||||
| 87 | |||||||
| 88 - 179 | =head1 SYNOPSIS
use Geo::Coder::Free;
my $geo = Geo::Coder::Free->new();
my $pt = $geo->geocode(location => 'Ramsgate, Kent, UK');
printf "%.6f, %.6f\n", $pt->lat(), $pt->long();
# With OpenAddresses/WhoOnFirst data:
my $geo2 = Geo::Coder::Free->new(openaddr => $ENV{OPENADDR_HOME});
my $pt2 = $geo2->geocode(location => '1600 Pennsylvania Avenue NW, Washington DC, USA');
# Free-text scanning:
my @hits = $geo2->geocode(scantext => 'She grew up in Ramsgate, Kent.',
region => 'GB');
=head1 DESCRIPTION
C<Geo::Coder::Free> translates addresses into latitude/longitude coordinates
using local SQLite databases built from free data sources - MaxMind/GeoNames,
OpenAddresses, Who's On First, OpenStreetMap, and dr5hn's countries/states/cities
database. It deliberately avoids paid or rate-limited online geocoding services.
The module is designed to be flexible, supporting both command-line and programmatic usage.
It also includes a sample CGI script for a web-based geocoding service.
Geocoding dispatch order depends on whether C<OPENADDR_HOME> (or C<openaddr>) is set:
B<With OpenAddresses data:>
=over 4
=item 1. C<Geo::Coder::Free::OpenAddresses> - requires C<OPENADDR_HOME>
=item 2. C<Geo::Coder::Free::Local> - user-curated CSV entries (tried as fallback)
=item 3. C<Geo::Coder::Free::MaxMind> - bundled, always available
=back
B<Without OpenAddresses data:>
=over 4
=item 1. C<Geo::Coder::Free::MaxMind> only - Local is not consulted.
=back
The C<cgi-bin> directory contains a simple DIY geo-coding website:
cgi-bin/page.fcgi page=query q=1600+Pennsylvania+Avenue+NW+Washington+DC+USA
The sample website is currently down while a new host is sought.
When it returns, you will be able to test it with:
curl 'https://geocode.nigelhorne.com/cgi-bin/page.fcgi?page=query&q=1600+Pennsylvania+Avenue+NW+Washington+DC+USA'
=head1 LIMITATIONS
=over 4
=item * C<scantext> mode only finds locations in OpenAddresses; it falls back
silently when C<OPENADDR_HOME> is not set (B<FIXME>: should warn).
=item * The C<__DATA__> alternatives table is hard-coded; it should live in a
user-editable config file.
=item * The address-regex scantext path misses birth-year sentences such as
C<"She was born May 21, 1937 in Noblesville, IN."> because the regex requires
a preceding capital-letter word directly before the city.
=item * C<reverse_geocode> is only partially implemented; the MaxMind path does
not return meaningful results.
=item * Without OpenAddresses data, a two-part C<"City, Country"> location is
often answered wrongly. The MaxMind backend reads the first part as a region
when it can, and answers with the first row it finds in that region, not with
the city: C<"Berlin, Germany"> and C<"Munich, Germany"> both come back at
52.08, 7.38, and C<"Toronto, Canada"> comes back in British Columbia. Some
others, such as C<"Paris, France">, are not found at all. These results carry
C<< accuracy => 'region' >> and C<< confidence => 0.1 >>, so a caller that needs
a city should check L</provenance> and reject them. Naming the region as well,
as in C<"Munich, Bavaria, Germany">, gives the right answer.
=item * Place names are matched as they are spelled in the data, with no
folding of accents: C<"ZE<uuml>rich, Switzerland"> is found and C<"Zurich, Switzerland">
is not. Pass locations as Perl character strings (decoded, for example by
C<use utf8> or an C<:encoding(UTF-8)> layer), not as undecoded UTF-8 bytes,
since a byte string does not match the name in the data.
=back
=cut | ||||||
| 180 | |||||||
| 181 | # ----------------------------------------------------------------------- | ||||||
| 182 | # Module-level singletons - initialised once, shared across all instances. | ||||||
| 183 | # Using 'our' so that test code can reset them between test runs if needed. | ||||||
| 184 | # ----------------------------------------------------------------------- | ||||||
| 185 | our $alternatives; | ||||||
| 186 | |||||||
| 187 | # The alternatives being tried by geocode() calls further up the stack. A key | ||||||
| 188 | # is skipped while it is in here, so an entry whose replacement still matches | ||||||
| 189 | # it (or two entries that rewrite into each other) cannot recurse forever. | ||||||
| 190 | my %_alternatives_active; | ||||||
| 191 | |||||||
| 192 | # ----------------------------------------------------------------------- | ||||||
| 193 | # Error-message table. All user-facing strings live here so that a future | ||||||
| 194 | # i18n layer only needs to swap this hash, not touch every call site. | ||||||
| 195 | # ----------------------------------------------------------------------- | ||||||
| 196 | my %_MESSAGES = ( | ||||||
| 197 | usage_geocode => 'Usage: %s::geocode(location => $location|scantext => $text)', | ||||||
| 198 | usage_geocode_many => 'Usage: %s::geocode_many(\@values, region => $region, on_error => "croak"|"undef")', | ||||||
| 199 | usage_reverse => 'Usage: %s::reverse_geocode(latlng => "$lat,$long")', | ||||||
| 200 | invalid_location => '%s: invalid location to geocode(), %s', | ||||||
| 201 | invalid_scantext => '%s: invalid scantext to geocode(), %s', | ||||||
| 202 | invalid_on_error => '%s: on_error must be "croak" or "undef", not %s', | ||||||
| 203 | reserved_location => '%s: "%s" is one of the keys geocode_many() answers with, so it cannot be one of the values it looks up', | ||||||
| 204 | geocoding_failed => '%s: geocoding failed', | ||||||
| 205 | reverse_unsupported => 'Reverse lookup is not yet supported', | ||||||
| 206 | use_arrow_new => '%s: use ->new() not ::new() to instantiate', | ||||||
| 207 | bad_ref_arg => 'Usage: %s â do not pass a non-hash reference', | ||||||
| 208 | ); | ||||||
| 209 | |||||||
| 210 | # Confidence thresholds for scantext results, expressed as named constants | ||||||
| 211 | # rather than magic numbers so call sites document intent. | ||||||
| 212 | Readonly::Scalar my $CONF_TRIPLET => 0.8; | ||||||
| 213 | Readonly::Scalar my $CONF_DUPLET => 0.7; | ||||||
| 214 | Readonly::Scalar my $CONF_REGEX => 0.7; | ||||||
| 215 | |||||||
| 216 | # The keys geocode_many() answers with besides the values themselves. The map | ||||||
| 217 | # and the counts share one hashref, so a value spelled like one of these would | ||||||
| 218 | # overwrite what the name stands for, and it is refused by name instead | ||||||
| 219 | Readonly::Array my @RESULT_KEYS => qw(lookups reads resolved unresolved unresolved_values); | ||||||
| 220 | |||||||
| 221 | # The backends geocode_many() reports its reads for: the slots of the facade | ||||||
| 222 | # that hold them, and the name each is reported under. The names are the short | ||||||
| 223 | # vocabulary provenance() reports a result's backend in, so that the backend | ||||||
| 224 | # that read a value and the backend a result came from are spelled the same way | ||||||
| 225 | # and a caller can read the two together. | ||||||
| 226 | Readonly::Hash my %_BACKEND_NAMES => ( | ||||||
| 227 | maxmind => 'MaxMind', | ||||||
| 228 | local => 'Local', | ||||||
| 229 | openaddr => 'OpenAddresses', | ||||||
| 230 | ); | ||||||
| 231 | |||||||
| 232 | # Stopwords excluded from word-window scantext searches. | ||||||
| 233 | my %_COMMON_WORDS = map { $_ => 1 } qw( | ||||||
| 234 | a age an and at be by cross for how i in is more of on or over pm | ||||||
| 235 | road she side some the to was with | ||||||
| 236 | ); | ||||||
| 237 | |||||||
| 238 - 291 | =head1 METHODS
=head2 new
=head3 SYNOPSIS
my $geo = Geo::Coder::Free->new();
my $geo = Geo::Coder::Free->new(openaddr => '/data/openaddr');
my $geo = Geo::Coder::Free->new(directory => '/data/maxmind');
=head3 DESCRIPTION
Constructor. Accepts a hash or hashref of options. If called without
C<openaddr>, the module checks C<$ENV{OPENADDR_HOME}> before giving up.
If called on an existing object instance (C<< $clone = $geo->new() >>), returns
a B<shallow clone>. All scalar fields are copied by value, but reference-type
fields (C<alternatives>, C<scantext_misses>, C<maxmind>, C<openaddr>) share the
same underlying object or hashref between the original and the clone. Mutations
to those shared references are immediately visible in both objects.
=head3 API SPECIFICATION
=head4 input
# Input schema (Params::Validate::Strict)
openaddr => { type => 'scalar', optional => 1 } # path to OpenAddresses/WOF data dir
directory => { type => 'scalar', optional => 1 } # path to MaxMind/GeoNames files
cache => { type => 'object', optional => 1, can => ['get', 'set'] } # CHI-compatible cache object
libpostal => { type => 'boolean', optional => 1, default => 1 } # false: OpenAddresses never uses Geo::libpostal
C<libpostal> is passed to L<Geo::Coder::Free::OpenAddresses>; see its
"Optional modules" section for why you might turn it off.
=head4 output
# Output schema (Return::Set)
{ type => 'object', isa => 'Geo::Coder::Free' }
=head3 EXAMPLE
use Geo::Coder::Free;
# Minimal - uses only the bundled MaxMind data:
my $geo = Geo::Coder::Free->new();
# Full - also searches OpenAddresses/WOF:
my $geo = Geo::Coder::Free->new(openaddr => $ENV{OPENADDR_HOME});
=head3 MESSAGES
use ->new() not ::new() Called as a function; use arrow syntax.
=cut | ||||||
| 292 | |||||||
| 293 | sub new { | ||||||
| 294 | 125 | 1 | 658389 | my $class = shift; | |||
| 295 | |||||||
| 296 | 125 | 196 | my $params = Params::Get::get_params(undef, @_) // {}; | ||||
| 297 | |||||||
| 298 | # Called as a function (Geo::Coder::Free::new) rather than a method | ||||||
| 299 | 125 | 1496 | if (!defined($class)) { | ||||
| 300 | 5 5 | 5 11 | if (keys %{$params}) { | ||||
| 301 | 2 | 10 | Carp::carp(_i18n('use_arrow_new', __PACKAGE__)); | ||||
| 302 | 2 | 196 | return; | ||||
| 303 | } | ||||||
| 304 | 3 | 3 | $class = __PACKAGE__; # FIXME: only works with no arguments | ||||
| 305 | } elsif (Scalar::Util::blessed($class)) { | ||||||
| 306 | 7 7 7 | 7 11 46 | return bless { %{$class}, %{$params} }, ref($class); | ||||
| 307 | } | ||||||
| 308 | |||||||
| 309 | # Populate the alternatives map once from __DATA__ and cache it | ||||||
| 310 | # across all instances. Each line is "from = to"; the text either side | ||||||
| 311 | # is kept as written (Config::Auto, used before, split a value into words, | ||||||
| 312 | # so "Borough of North Tyneside" became "Borough, of, North, Tyneside"). | ||||||
| 313 | 116 | 119 | if (!$alternatives) { | ||||
| 314 | 10 | 8 | $alternatives = {}; | ||||
| 315 | 10 | 24 | while (my $line = <DATA>) { | ||||
| 316 | 40 | 47 | next if $line =~ /^\s*(#|$)/; | ||||
| 317 | 40 | 80 | my ($from, $to) = split /\s*=\s*/, $line, 2; | ||||
| 318 | 40 | 30 | next unless defined($to); | ||||
| 319 | 40 | 163 | s/^\s+|\s+$//g for $from, $to; | ||||
| 320 | 40 | 122 | $alternatives->{$from} = $to if length($from) && length($to); | ||||
| 321 | } | ||||||
| 322 | } | ||||||
| 323 | |||||||
| 324 | # Resolve OPENADDR_HOME before Object::Configure so plug-ins can see it | ||||||
| 325 | 116 | 248 | if (!defined($params->{'openaddr'}) && $ENV{'OPENADDR_HOME'}) { | ||||
| 326 | 18 | 21 | $params->{'openaddr'} = $ENV{'OPENADDR_HOME'}; | ||||
| 327 | } | ||||||
| 328 | |||||||
| 329 | 116 | 170 | $params = Object::Configure::configure($class, $params); | ||||
| 330 | |||||||
| 331 | my $rc = { | ||||||
| 332 | 116 116 | 507565 403 | %{$params}, | ||||
| 333 | maxmind => Geo::Coder::Free::MaxMind->new($params), | ||||||
| 334 | alternatives => $alternatives, | ||||||
| 335 | }; | ||||||
| 336 | |||||||
| 337 | 116 | 45667 | if ($params->{'openaddr'}) { | ||||
| 338 | 30 30 | 25 103 | $rc->{'openaddr'} = Geo::Coder::Free::OpenAddresses->new(id => 'md5', %{$params}); | ||||
| 339 | } | ||||||
| 340 | 114 | 196 | if (my $cache = $params->{'cache'}) { | ||||
| 341 | 8 | 9 | $rc->{'cache'} = $cache; | ||||
| 342 | } | ||||||
| 343 | |||||||
| 344 | 114 | 324 | return bless $rc, $class; | ||||
| 345 | } | ||||||
| 346 | |||||||
| 347 - 398 | =head2 valid_location
=head3 SYNOPSIS
if ($geo->valid_location($row->{'place'})) {
push @found, $geo->geocode(location => $row->{'place'});
}
=head3 DESCRIPTION
Answers whether a string is something this module will even try to look up.
It is the public form of the rule C<geocode()> applies before it touches a
backend, so a caller working down a spreadsheet column can skip a row whose
value is a bare number - or is empty - instead of catching the croak that
would otherwise kill the whole run. That rule is defined once, in
L<Geo::Location::Predicate>, and the backends apply it too: what
C<valid_location()> refuses is refused wherever it is looked up.
Only that cheap syntactic test is answered. A true result means the string is
not disqualified, B<not> that a backend knows it: C<'Ramsgate, Kent'> is valid
and may still find nothing, while C<'0044'> is invalid and is never looked up.
The rule rejects a string of digits only, which is what a numeric column holds
when the wrong column has been read, so anything with a non-digit in it counts
as valid - C<'3.14'> and C<'-'> included. C<undef>, C<''> and a reference are
not valid.
=head3 API SPECIFICATION
=head4 input
location => { type => 'scalar', optional => 1 } # the string to test
=head4 output
{ type => 'boolean' } # false for undef, '' and a digit-only string
=head3 EXAMPLE
my $geo = Geo::Coder::Free->new();
$geo->valid_location('Ramsgate, Kent'); # true
$geo->valid_location('0044'); # false - never a location
$geo->valid_location(''); # false
$geo->valid_location(undef); # false
Accepts the three calling conventions the rest of this module does:
$geo->valid_location('0044');
Geo::Coder::Free->valid_location('0044');
Geo::Coder::Free::valid_location('0044');
=cut | ||||||
| 399 | |||||||
| 400 | sub valid_location { | ||||||
| 401 | 15 | 1 | 352 | my $self = shift; | |||
| 402 | |||||||
| 403 | # The location is whichever argument is not the invocant, so that the | ||||||
| 404 | # predicate reads the same whether it is called on an instance, on the | ||||||
| 405 | # class or as a plain function. | ||||||
| 406 | 15 | 13 | my $location; | ||||
| 407 | 15 | 27 | if (ref($self)) { | ||||
| 408 | 11 | 8 | $location = shift; | ||||
| 409 | } elsif (@_) { | ||||||
| 410 | 2 | 2 | $location = shift; | ||||
| 411 | } elsif (defined($self) && $self ne __PACKAGE__) { | ||||||
| 412 | 2 | 3 | $location = $self; | ||||
| 413 | } | ||||||
| 414 | |||||||
| 415 | 15 | 26 | return _valid_location($location) ? 1 : 0; | ||||
| 416 | } | ||||||
| 417 | |||||||
| 418 - 526 | =head2 geocode
=head3 SYNOPSIS
# Standard lookup (returns a Geo::Location::Point or undef)
my $pt = $geo->geocode(location => 'Ramsgate, Kent, UK');
printf "lat=%.6f lon=%.6f\n", $pt->lat(), $pt->long();
# Scantext - returns a list of Geo::Location::Point objects
my @hits = $geo->geocode(
scantext => 'She lived in Ramsgate, Kent.',
region => 'GB',
ignore_words => [qw(lived)],
);
# Invocation flexibility (all equivalent)
$geo->geocode('Ramsgate, Kent, UK');
$geo->geocode({ location => 'Ramsgate, Kent, UK' });
$geo->geocode(location => 'Ramsgate, Kent, UK');
# A batch caller with a dirty column: skip the row, do not kill the run
for my $place (@column) {
my $pt = $geo->geocode(location => $place, on_error => 'undef');
next unless defined $pt;
push @found, $pt;
}
=head3 DESCRIPTION
A location or a scantext that is not a location at all - a bare number, such as
the C<0044> a numeric column holds - is refused before any backend is
touched. C<valid_location()> answers the same question, so a caller can filter
first; C<on_error> chooses what happens when one gets through anyway:
=over 4
=item * C<'croak'> (the default) - C<Carp::croak>, the behaviour every existing
caller already has.
=item * C<'undef'> - return C<undef>, so one bad row in a spreadsheet column is
skipped rather than ending the run. It changes the answer for B<only> the two
documented input errors below; an exception from a backend is still an
exception, and a malformed argument list still croaks.
=back
The option is deliberately narrow, so a batch caller wants C<valid_location()>
with it: that predicate answers false for C<undef> and C<''> as well, which are
the other two things an empty cell holds. A call with neither a location nor a
scantext is a usage error rather than an input error - the same croak as calling
C<geocode()> with no arguments - and stays a croak on either setting.
=head3 API SPECIFICATION
=head4 input
# Input schema (Params::Validate::Strict) - exactly one of location or scantext is required
location => { type => 'scalar', optional => 1 } # address string (exclusive with scantext)
scantext => { type => 'scalar', optional => 1 } # free text to scan for place names
region => { type => 'scalar', optional => 1 } # ISO 3166-1 alpha-2 country code hint
ignore_words => { type => 'arrayref', optional => 1 } # words to suppress during scantext scan
on_error => { type => 'scalar', optional => 1 } # 'croak' (default) or 'undef'
=head4 output
# Output schema (Return::Set)
# scalar context: { type => 'object', isa => 'Geo::Location::Point', optional => 1 }
# list context: { type => 'arrayref', of => { isa => 'Geo::Location::Point' } }
=head4 provenance
Every result carries C<geocoder>, C<database>, C<confidence> and C<accuracy>
which distribution produced
it, which backend answered, how good the match was, and how precise the
coordinate is. C<provenance()> reads the four back. C<location> is set by every
path but the MaxMind cities one, and C<text> only by a scantext lookup; C<lat>,
C<long> and C<lng> come from the value object itself.
=head3 MESSAGES
Usage: ...::geocode(...) No location or scantext argument given.
invalid location to geocode() location is purely numeric.
invalid scantext to geocode() scantext is purely numeric.
on_error must be ... on_error is neither 'croak' nor 'undef'.
=head3 PSEUDOCODE
if self is not a blessed object -> delegate to new()->geocode(@args)
normalise @_ into %params
validate: on_error is 'croak' or 'undef' (croak if not)
validate: location is not purely numeric; scantext is not purely numeric
(undef instead of croaking when on_error is 'undef')
if openaddr backend is available:
if scantext:
try the raw scantext string as a direct location
build stopword set from %_COMMON_WORDS + ignore_words param
try 3-word windows (triplets) at confidence 0.8
try 2-word windows (duplets) at confidence 0.7
try the address-pattern regex at confidence 0.7
try region-specific address finders (GB / US / CA)
mark scantext as a miss; return undef
else:
try openaddr backend
try local backend
try __DATA__ alternatives map
try maxmind backend for location lookups
croak if no scantext and no location
=cut | ||||||
| 527 | |||||||
| 528 | sub geocode { | ||||||
| 529 | 149 | 1 | 39336 | my $self = shift; | |||
| 530 | |||||||
| 531 | # Handle being called as a function rather than a method | ||||||
| 532 | 149 | 247 | if (!ref($self)) { | ||||
| 533 | 8 | 18 | if (scalar @_) { | ||||
| 534 | 1 | 2 | return __PACKAGE__->new()->geocode(@_); | ||||
| 535 | } elsif (!defined($self)) { | ||||||
| 536 | 1 | 2 | Carp::croak(_i18n('usage_geocode', __PACKAGE__)); | ||||
| 537 | } elsif ($self eq __PACKAGE__) { | ||||||
| 538 | 5 | 3 | Carp::croak(_i18n('usage_geocode', $self)); | ||||
| 539 | } | ||||||
| 540 | 1 | 2 | return __PACKAGE__->new()->geocode($self); | ||||
| 541 | } elsif (ref($self) eq 'HASH') { | ||||||
| 542 | 1 | 2 | return __PACKAGE__->new()->geocode($self); | ||||
| 543 | } | ||||||
| 544 | |||||||
| 545 | 140 | 155 | my %params = _normalize_args(_i18n('usage_geocode', __PACKAGE__), 'location', @_); | ||||
| 546 | |||||||
| 547 | # What to do with an input this module rejects before it touches a backend. | ||||||
| 548 | # 'croak' is the historical behaviour and stays the default, so no existing | ||||||
| 549 | # caller changes; 'undef' is for a batch caller working down a dirty column, | ||||||
| 550 | # which wants the row skipped rather than the whole run killed. Only the two | ||||||
| 551 | # input errors below consult it: a backend that throws still throws, because | ||||||
| 552 | # nothing here is wrapped in an eval | ||||||
| 553 | 130 | 196 | my $on_error = $params{'on_error'} // 'croak'; | ||||
| 554 | 130 | 169 | Carp::croak(_i18n('invalid_on_error', __PACKAGE__, $on_error)) | ||||
| 555 | unless $on_error eq 'croak' || $on_error eq 'undef'; | ||||||
| 556 | |||||||
| 557 | # Reject pure-numeric inputs early - they are never valid addresses. | ||||||
| 558 | # The rule is Geo::Location::Predicate's, the same one the backends | ||||||
| 559 | # apply, and valid_location() answers it in public, so a caller can | ||||||
| 560 | # pre-filter with it and never reach this croak at all. | ||||||
| 561 | 128 | 219 | if (defined($params{'location'}) && !_valid_location($params{'location'})) { | ||||
| 562 | 37 | 184 | return if !length($params{'location'}) || ($on_error eq 'undef'); | ||||
| 563 | 16 | 17 | Carp::croak(_i18n('invalid_location', __PACKAGE__, $params{'location'})); | ||||
| 564 | } | ||||||
| 565 | 91 | 342 | if (defined($params{'scantext'}) && !_valid_location($params{'scantext'})) { | ||||
| 566 | 8 | 48 | return if !length($params{'scantext'}) || ($on_error eq 'undef'); | ||||
| 567 | 4 | 7 | Carp::croak(_i18n('invalid_scantext', __PACKAGE__, $params{'scantext'})); | ||||
| 568 | } | ||||||
| 569 | |||||||
| 570 | 83 | 188 | if ($self->{'openaddr'}) { | ||||
| 571 | 40 | 59 | if (my $scantext = $params{'scantext'}) { | ||||
| 572 | 11 | 20 | return if $self->{'scantext_misses'}{$scantext}; | ||||
| 573 | |||||||
| 574 | # First try the whole scantext as a direct location lookup - | ||||||
| 575 | # saves work when the text happens to be a valid address string. | ||||||
| 576 | 7 | 20 | $self->{'local'} ||= Geo::Coder::Free::Local->new(); | ||||
| 577 | 14 | 30 | my @direct = grep { defined } | ||||
| 578 | $self->{'local'}->geocode($scantext), | ||||||
| 579 | $self->{'openaddr'}->geocode($scantext), | ||||||
| 580 | 7 | 33 | $self->{'maxmind'}->geocode($scantext); | ||||
| 581 | 7 | 7 | return @direct if @direct; | ||||
| 582 | |||||||
| 583 | 7 | 6 | my $region = $params{'region'}; | ||||
| 584 | |||||||
| 585 | # Build the effective stopword set. When the caller supplies no | ||||||
| 586 | # ignore_words we avoid copying %_COMMON_WORDS (which is immutable | ||||||
| 587 | # at runtime) by using a reference to it directly. Only when extra | ||||||
| 588 | # words are needed do we allocate and populate a merged hash. | ||||||
| 589 | 7 | 5 | my $ignore_words; | ||||
| 590 | 7 | 6 | if (my $iw = $params{'ignore_words'}) { | ||||
| 591 | 1 | 9 | my %merged = %_COMMON_WORDS; | ||||
| 592 | 1 1 | 1 3 | $merged{lc $_} = 1 for @{$iw}; | ||||
| 593 | 1 | 1 | $ignore_words = \%merged; | ||||
| 594 | } else { | ||||||
| 595 | 6 | 6 | $ignore_words = \%_COMMON_WORDS; | ||||
| 596 | } | ||||||
| 597 | |||||||
| 598 | # 3-word window search | ||||||
| 599 | 7 | 42 | my @triplets = _find_word_ngrams($scantext, 3, $ignore_words); | ||||
| 600 | 7 | 12 | my $res = $self->_resolve_scan_candidates(\@triplets, $region, $CONF_TRIPLET, $scantext); | ||||
| 601 | 7 7 | 3 9 | if (@{$res}) { | ||||
| 602 | 0 0 | 0 0 | return wantarray ? @{$res} : $res->[0]; | ||||
| 603 | } | ||||||
| 604 | |||||||
| 605 | # 2-word window search | ||||||
| 606 | 7 | 8 | my @duplets = _find_word_ngrams($scantext, 2, $ignore_words); | ||||
| 607 | 7 | 8 | $res = $self->_resolve_scan_candidates(\@duplets, $region, $CONF_DUPLET, $scantext); | ||||
| 608 | 7 7 | 5 8 | if (@{$res}) { | ||||
| 609 | 0 0 | 0 0 | return wantarray ? @{$res} : $res->[0]; | ||||
| 610 | } | ||||||
| 611 | |||||||
| 612 | # Regex-based address pattern - catches "City, ST"-style fragments. | ||||||
| 613 | # Note: misses sentences like "born May 21, 1937 in Noblesville, IN" | ||||||
| 614 | # because the pattern requires a capitalised word before the city. | ||||||
| 615 | 7 | 8 | my @regex_matches = $scantext =~ | ||||
| 616 | /\b(?:\d+\s+)?(?:[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\.?),\s* | ||||||
| 617 | (?:[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*(?:,\s*[A-Z]{2,})*)\b/gx; | ||||||
| 618 | 7 0 | 6 0 | my @places = grep { defined && $_ ne '' } @regex_matches; | ||||
| 619 | 7 | 7 | $res = $self->_resolve_scan_candidates(\@places, $region, $CONF_REGEX, $scantext); | ||||
| 620 | 7 7 | 5 7 | if (@{$res}) { | ||||
| 621 | 0 0 | 0 0 | return wantarray ? @{$res} : $res->[0]; | ||||
| 622 | } | ||||||
| 623 | |||||||
| 624 | # Region-specific structured address patterns | ||||||
| 625 | 7 | 6 | if ($region) { | ||||
| 626 | 0 | 0 | my @candidates; | ||||
| 627 | 0 0 | 0 0 | if ($region eq 'GB') { @candidates = _find_gb_addresses($scantext) } | ||||
| 628 | 0 | 0 | elsif ($region eq 'US') { @candidates = _find_us_addresses($scantext) } | ||||
| 629 | 0 | 0 | elsif ($region eq 'Canada') { @candidates = _find_ca_addresses($scantext) } | ||||
| 630 | |||||||
| 631 | 0 | 0 | if (@candidates) { | ||||
| 632 | 0 | 0 | my @regional; | ||||
| 633 | 0 | 0 | for my $candidate (@candidates) { | ||||
| 634 | 0 | 0 | next if $ignore_words->{lc $candidate}; | ||||
| 635 | 0 | 0 | my @hits = grep { defined } | ||||
| 636 | 0 | 0 | $self->{'openaddr'}->geocode("$candidate, $region"); | ||||
| 637 | 0 | 0 | push @regional, @hits if @hits; | ||||
| 638 | } | ||||||
| 639 | 0 | 0 | return @regional if @regional; | ||||
| 640 | } | ||||||
| 641 | } | ||||||
| 642 | |||||||
| 643 | 7 | 8 | $self->{'scantext_misses'}{$scantext} = 1; | ||||
| 644 | 7 | 20 | return; | ||||
| 645 | } | ||||||
| 646 | |||||||
| 647 | # Standard (non-scantext) lookup path | ||||||
| 648 | 29 | 21 | if (wantarray) { | ||||
| 649 | 1 | 3 | my @rc = $self->{'openaddr'}->geocode(\%params); | ||||
| 650 | 1 | 4 | return @rc if @rc && $rc[0]; | ||||
| 651 | 0 | 0 | $self->{'local'} ||= Geo::Coder::Free::Local->new(); | ||||
| 652 | 0 | 0 | @rc = $self->{'local'}->geocode(\%params); | ||||
| 653 | 0 | 0 | return @rc if @rc && $rc[0]; | ||||
| 654 | } else { | ||||||
| 655 | 28 | 49 | if (my $rc = $self->{'openaddr'}->geocode(\%params)) { | ||||
| 656 | 8 | 104 | return $rc; | ||||
| 657 | } | ||||||
| 658 | 17 | 101 | $self->{'local'} ||= Geo::Coder::Free::Local->new(); | ||||
| 659 | 17 | 52 | if (my $rc = $self->{'local'}->geocode(\%params)) { | ||||
| 660 | 4 | 36 | return $rc; | ||||
| 661 | } | ||||||
| 662 | } | ||||||
| 663 | |||||||
| 664 | # Try the alternatives table - hand-curated mappings for locations that | ||||||
| 665 | # the databases have under slightly different names. | ||||||
| 666 | # M3 (transitive reduction): every scantext path inside this block returned | ||||||
| 667 | # early above; at this point $params{'scantext'} is provably falsy, so the | ||||||
| 668 | # former `if (!$params{'scantext'})` guard was a tautology - removed. | ||||||
| 669 | 13 | 25 | if (my $alt = $self->{'alternatives'}) { | ||||
| 670 | 13 | 41 | my $location = $params{'location'}; | ||||
| 671 | # Not each(): the recursive geocode() calls below walk the same | ||||||
| 672 | # hash, sharing and resetting its iterator, so an each() loop here | ||||||
| 673 | # would start again from the top and retry the same key forever. | ||||||
| 674 | 13 13 | 10 32 | foreach my $key (sort keys %{$alt}) { | ||||
| 675 | 36 | 28 | next if $_alternatives_active{$key}; | ||||
| 676 | 33 | 24 | my $value = $alt->{$key}; | ||||
| 677 | 33 | 130 | next unless $location =~ $key; | ||||
| 678 | 5 | 7 | local $_alternatives_active{$key} = 1; | ||||
| 679 | 5 | 18 | (my $new_loc = $location) =~ s/$key/$value/; | ||||
| 680 | 5 | 7 | $params{'location'} = $new_loc; | ||||
| 681 | 5 | 15 | if (my $rc = $self->geocode(\%params)) { | ||||
| 682 | 3 | 14 | return $rc; | ||||
| 683 | } | ||||||
| 684 | # Also try the comma-free variant ("Tyne and Wear" etc.) | ||||||
| 685 | 2 | 8 | if ($value =~ /, /) { | ||||
| 686 | 1 | 2 | (my $flat = $value) =~ s/,//g; | ||||
| 687 | 1 | 7 | ($new_loc = $location) =~ s/$key/$flat/; | ||||
| 688 | 1 | 1 | $params{'location'} = $new_loc; | ||||
| 689 | 1 | 3 | if (my $rc = $self->geocode(\%params)) { | ||||
| 690 | 0 | 0 | return $rc; | ||||
| 691 | } | ||||||
| 692 | } | ||||||
| 693 | # Restore for the next iteration | ||||||
| 694 | 2 | 7 | $params{'location'} = $location; | ||||
| 695 | } | ||||||
| 696 | } | ||||||
| 697 | } | ||||||
| 698 | |||||||
| 699 | # Final fallback: MaxMind for location lookups. | ||||||
| 700 | # Scantext without OPENADDR_HOME will silently reach here and return undef; | ||||||
| 701 | # a proper fix would warn here (see LIMITATIONS). | ||||||
| 702 | # M8 (tautology elimination): both arms of the former wantarray ternary were | ||||||
| 703 | # identical - collapsed to a single call; context propagates implicitly. | ||||||
| 704 | 53 | 59 | if ($params{'location'}) { | ||||
| 705 | 44 | 72 | return $self->{'maxmind'}->geocode(\%params); | ||||
| 706 | } | ||||||
| 707 | |||||||
| 708 | 9 | 16 | Carp::croak(_i18n('usage_geocode', __PACKAGE__)) unless $params{'scantext'}; | ||||
| 709 | 3 | 7 | return; | ||||
| 710 | } | ||||||
| 711 | |||||||
| 712 - 858 | =head2 geocode_many
=head3 SYNOPSIS
my $batch = $geo->geocode_many(\@places, region => 'GB', on_error => 'undef');
for my $place (@places) {
my $pt = $batch->{$place}; # a Geo::Location::Point, or undef
next unless defined $pt;
printf "%s is at %.4f, %.4f\n", $place, $pt->lat(), $pt->long();
}
printf "geocoded %d of %d places in %d lookups\n",
$batch->{'resolved'},
$batch->{'resolved'} + $batch->{'unresolved'},
$batch->{'lookups'};
=head3 DESCRIPTION
The loop a batch caller writes around C<geocode()>, run for it: one lookup per
B<distinct> value, with the same options and the same results, plus the counts
that caller would otherwise keep itself - how many values made a backend read,
how many resolved, how many did not, which ones, and which backend read for them
The hashref returned holds one entry per distinct value given, keyed by the
value, plus five keys of its own:
=over 4
=item * C<< $batch->{$value} >> - the C<Geo::Location::Point> the value resolved
to, or C<undef> if nothing did. Every distinct value in the input is keyed, so
a caller walking a column can look each row up without first asking whether the
batch covered it. An C<undef> in the input has no key of its own and shares the
entry of the empty string, which is not a location either.
=item * C<lookups> - how many distinct values made a backend read to answer
them: one per value, however many backends looked at it. A value the module
refuses before any backend is touched - a bare number, C<''>, C<undef>, a
reference - is not counted, and neither is a value a backend answered from a
cache or from a hand-curated entry, because nothing was read for it. This is
what makes the count the work a caller is capping rather than the work it asked
for. A value repeated in the batch is one lookup. Every backend reports its
reads through C<reads()>, and a value every backend has already missed is
remembered in L<Geo::Coder::Free::MissCache>, one process-level cache that all
backends and instances share, whose entries expire after an hour and can be
emptied early with L</clear_miss_cache>. A later batch in the same process is
answered from those caches and so reports the reads it needed, often none, in
place of the ones the first batch made.
=item * C<reads> - how many reads each backend made for the batch, keyed by the
name C<provenance()> reports that backend under: C<MaxMind>, C<Local> or
C<OpenAddresses>. Every backend the object holds is named, at zero for one that
read nothing, so a batch answered from a cache names its backends at zero rather
than naming none. C<lookups> counts values and this counts reads - a value that
made two backends read is one lookup and a read for each of them, so these
numbers can add up to more than C<lookups> - and the two vocabularies are the
same, so a caller can put the backend that read a value beside the backend a
result came from.
=item * C<resolved> - how many distinct values produced a point.
=item * C<unresolved> - how many produced nothing, the refused values included.
=item * C<unresolved_values> - those values, in the order they were first seen,
so that a caller can record them as tried-and-failed rather than retrying them
on every request. Section 2.5 sketches this list under the name C<unresolved>;
here C<unresolved> is the count a "geocoded N of M" message needs and the values
themselves are C<unresolved_values>.
=back
C<resolved> and C<unresolved> always add up to the number of distinct values
given, and the five keys above are reserved: a value equal to one of them is
refused by name, before anything is looked up, rather than allowed to overwrite
a count.
C<region> and C<on_error> are handed to every lookup and mean there what they
mean in C<geocode()>, including the rule that a backend exception is still an
exception - nothing here is wrapped in an C<eval>. C<on_error> defaults to
C<'croak'> for the same reason it does there: a batch should not quietly plot
fewer points than its column holds unless the caller asked it to, and a caller
working down a dirty column asks for C<'undef'>. The two settings do not differ
in which values are looked up, only in what happens to a value the module
refuses: C<'croak'> dies on it with the message C<geocode()> gives for it,
C<'undef'> counts it unresolved and carries on.
A batch of no values is not an error: the counts come back zero.
The values are treated as a set. Repeating a value in the input looks it up
once and every row is keyed to the same point object, so a column that repeats
the same handful of places thousands of times costs the handful of lookups.
=head3 API SPECIFICATION
=head4 input
# Input schema (Params::Validate::Strict) - values required, options optional
values => { type => 'arrayref' } # location strings, one per row
region => { type => 'scalar', optional => 1 } # ISO 3166-1 alpha-2 country code hint
on_error => { type => 'scalar', optional => 1 } # 'croak' (the default) or 'undef'
=head4 output
# Output schema (Return::Set)
{ type => 'hashref' }
# $result->{$value} => { isa => 'Geo::Location::Point', optional => 1 }
# $result->{lookups} => { type => 'integer' }
# $result->{reads} => { type => 'hashref' }
# $result->{resolved} => { type => 'integer' }
# $result->{unresolved} => { type => 'integer' }
# $result->{unresolved_values} => { type => 'arrayref' }
=head3 MESSAGES
Usage: ...::geocode_many(...) No values argument, one that is not an arrayref,
or options that are neither a list nor a hashref.
invalid location to geocode() A value refused under on_error => 'croak', with the
message geocode() gives for that value.
on_error must be ... on_error is neither 'croak' nor 'undef'.
<value> is one of the keys ... A value spelled like one of the five returned keys.
=head3 EXAMPLE
my $geo = Geo::Coder::Free->new(openaddr => $ENV{OPENADDR_HOME});
# A log file's place column: repeats, a few unreadable values, one empty cell.
my @places = ('Ramsgate, Kent', '0044', 'Nowhere, Imaginary, ZZ',
'Ramsgate, Kent', undef);
my $batch = $geo->geocode_many(\@places, region => 'GB', on_error => 'undef');
printf "geocoded %d of %d distinct places in %d lookups\n",
$batch->{'resolved'},
$batch->{'resolved'} + $batch->{'unresolved'},
$batch->{'lookups'}; # 4 distinct places, 2 of them looked up
# Which backend did the reading, and which answered from a cache: a backend
# at zero answered nothing by reading.
while (my ($backend, $reads) = each %{$batch->{'reads'}}) {
print "$backend read $reads times\n";
}
for my $place (@{$batch->{'unresolved_values'}}) {
warn "no coordinates for '" . (defined($place) ? $place : '') . "'\n";
}
=cut | ||||||
| 859 | |||||||
| 860 | sub geocode_many { | ||||||
| 861 | 24 | 1 | 15719 | my $self = shift; | |||
| 862 | |||||||
| 863 | # The same calling conventions as the rest of the module, with the batch's | ||||||
| 864 | # own spelling for the plain-function form: the values are always an | ||||||
| 865 | # arrayref, so that is what a function call arrives holding. | ||||||
| 866 | 24 | 46 | if (!Scalar::Util::blessed($self)) { | ||||
| 867 | 3 | 7 | return __PACKAGE__->new()->geocode_many($self, @_) | ||||
| 868 | if ref($self) eq 'ARRAY'; | ||||||
| 869 | 2 | 4 | Carp::croak(_i18n('usage_geocode_many', __PACKAGE__)) unless @_; | ||||
| 870 | 1 | 3 | return __PACKAGE__->new()->geocode_many(@_); | ||||
| 871 | } | ||||||
| 872 | |||||||
| 873 | 21 | 20 | my $values = shift; | ||||
| 874 | 21 | 30 | Carp::croak(_i18n('usage_geocode_many', __PACKAGE__)) unless ref($values) eq 'ARRAY'; | ||||
| 875 | |||||||
| 876 | # Options are a hashref or an even list. A lone scalar is neither: there is | ||||||
| 877 | # no bare-string form of a batch, so it is refused rather than guessed at. | ||||||
| 878 | 19 | 13 | my $opts; | ||||
| 879 | 19 | 48 | if (@_ == 1 && ref($_[0]) eq 'HASH') { | ||||
| 880 | 0 0 | 0 0 | $opts = { %{$_[0]} }; | ||||
| 881 | 34 | 42 | } elsif ((scalar @_) % 2 == 0 && !grep { ref } @_) { | ||||
| 882 | 18 | 22 | $opts = { @_ }; | ||||
| 883 | } else { | ||||||
| 884 | 1 | 2 | Carp::croak(_i18n('usage_geocode_many', __PACKAGE__)); | ||||
| 885 | } | ||||||
| 886 | |||||||
| 887 | 18 | 27 | my $on_error = $opts->{'on_error'} // 'croak'; | ||||
| 888 | 18 | 37 | Carp::croak(_i18n('invalid_on_error', __PACKAGE__, $on_error)) | ||||
| 889 | unless $on_error eq 'croak' || $on_error eq 'undef'; | ||||||
| 890 | |||||||
| 891 | 16 | 11 | my $region = $opts->{'region'}; | ||||
| 892 | |||||||
| 893 | # A value spelled like one of the keys this method answers with would | ||||||
| 894 | # overwrite it, so it is refused by name - before anything is looked up, | ||||||
| 895 | # rather than after a count has been lost. | ||||||
| 896 | 16 80 | 31 259 | my %reserved = map { $_ => 1 } @RESULT_KEYS; | ||||
| 897 | 16 16 | 88 18 | for my $value (@{$values}) { | ||||
| 898 | 431 | 254 | my $key = defined($value) ? $value : ''; | ||||
| 899 | 431 | 284 | Carp::croak(_i18n('reserved_location', __PACKAGE__, $key)) if $reserved{$key}; | ||||
| 900 | } | ||||||
| 901 | |||||||
| 902 | 14 70 | 17 165 | my %results = map { $_ => 0 } @RESULT_KEYS; | ||||
| 903 | 14 | 48 | $results{'unresolved_values'} = []; | ||||
| 904 | 14 | 10 | $results{'reads'} = {}; | ||||
| 905 | |||||||
| 906 | # A batch is a set: a value repeated in the input is looked up once, and | ||||||
| 907 | # every row holding it is keyed to the same object. | ||||||
| 908 | 14 | 9 | my %seen; | ||||
| 909 | 14 14 | 8 11 | for my $value (@{$values}) { | ||||
| 910 | # undef has no key of its own, and the empty string it shares is not a | ||||||
| 911 | # location either, so it is unresolved without reaching a backend. | ||||||
| 912 | 428 | 237 | my $key = defined($value) ? $value : ''; | ||||
| 913 | 428 | 295 | next if $seen{$key}++; | ||||
| 914 | |||||||
| 915 | # What geocode() makes of this value on this setting is asked of | ||||||
| 916 | # geocode() rather than written out again here: the guard, its message and | ||||||
| 917 | # the choice of backend are all its own. The backends' read counters are | ||||||
| 918 | # taken around the call, so the value is counted as a lookup only if one of | ||||||
| 919 | # them actually read: a value answered from a cache, or from a hand-curated | ||||||
| 920 | # entry, is not counted as work the batch did not do. Taking them per | ||||||
| 921 | # backend rather than in total says which backend read, and which answered | ||||||
| 922 | # from somewhere else: a read is added to the name of the backend that made | ||||||
| 923 | # it, so a value two backends read through is one lookup and a read each. | ||||||
| 924 | 27 | 22 | my $reads_before = $self->_backend_reads(); | ||||
| 925 | 27 | 38 | my $point = $self->geocode( | ||||
| 926 | location => $key, | ||||||
| 927 | defined($region) ? (region => $region) : (), | ||||||
| 928 | on_error => $on_error, | ||||||
| 929 | ); | ||||||
| 930 | 24 | 94 | my $reads_after = $self->_backend_reads(); | ||||
| 931 | |||||||
| 932 | 24 | 17 | my $read = 0; | ||||
| 933 | 24 24 | 9 22 | foreach my $backend (keys %{$reads_after}) { | ||||
| 934 | 28 | 29 | my $reads = $reads_after->{$backend} - ($reads_before->{$backend} // 0); | ||||
| 935 | 28 | 21 | next unless $reads > 0; | ||||
| 936 | 13 | 10 | $results{'reads'}{$backend} += $reads; | ||||
| 937 | 13 | 11 | $read += $reads; | ||||
| 938 | } | ||||||
| 939 | |||||||
| 940 | # A value the module refuses is not a lookup: it never reached a backend, | ||||||
| 941 | # so it does not count against the work a caller is capping. | ||||||
| 942 | 24 | 21 | $results{'lookups'}++ if _valid_location($key) && $read; | ||||
| 943 | 24 | 76 | $results{$key} = $point; | ||||
| 944 | |||||||
| 945 | 24 | 19 | if (defined($point)) { | ||||
| 946 | 9 | 11 | $results{'resolved'}++; | ||||
| 947 | } else { | ||||||
| 948 | 15 | 8 | $results{'unresolved'}++; | ||||
| 949 | 15 15 | 9 19 | push @{$results{'unresolved_values'}}, $value; | ||||
| 950 | } | ||||||
| 951 | } | ||||||
| 952 | |||||||
| 953 | # A backend that read nothing is reported as well, at zero: what a caller is | ||||||
| 954 | # asking is which backend read and which answered from a cache, and a backend | ||||||
| 955 | # left out of the answer cannot be told from one that answered from one. | ||||||
| 956 | # Local is built the first time a lookup falls through to it, so it appears | ||||||
| 957 | # once it has been consulted, whether or not it found anything. | ||||||
| 958 | 11 11 | 7 10 | foreach my $backend (keys %{$self->_backend_reads()}) { | ||||
| 959 | 13 | 18 | $results{'reads'}{$backend} //= 0; | ||||
| 960 | } | ||||||
| 961 | |||||||
| 962 | 11 | 29 | return \%results; | ||||
| 963 | } | ||||||
| 964 | |||||||
| 965 | # How much reading each backend has done so far, by the name it is reported | ||||||
| 966 | # under. Counters rather than flags: the change across a call says which | ||||||
| 967 | # backend read for that value, and a backend that was never called cannot leave | ||||||
| 968 | # a stale flag behind. A backend object may not exist yet - Local is built on | ||||||
| 969 | # first use - and a test may hand the facade a stand-in, so each slot is asked | ||||||
| 970 | # only if it holds something that can answer. | ||||||
| 971 | sub _backend_reads { | ||||||
| 972 | 62 | 31 | my $self = shift; | ||||
| 973 | |||||||
| 974 | 62 | 33 | my %reads; | ||||
| 975 | 62 | 60 | foreach my $slot (sort keys %_BACKEND_NAMES) { | ||||
| 976 | 186 | 691 | my $backend = $self->{$slot} or next; | ||||
| 977 | 72 | 154 | next unless Scalar::Util::blessed($backend) && $backend->can('reads'); | ||||
| 978 | 72 | 68 | $reads{$_BACKEND_NAMES{$slot}} = $backend->reads() // 0; | ||||
| 979 | } | ||||||
| 980 | |||||||
| 981 | 62 | 78 | return \%reads; | ||||
| 982 | } | ||||||
| 983 | |||||||
| 984 - 1013 | =head2 clear_miss_cache
Geo::Coder::Free->clear_miss_cache();
$geo->clear_miss_cache();
Geo::Coder::Free->clear_miss_cache('OpenAddresses');
Forgets the lookups that found nothing. Each backend remembers a miss in
L<Geo::Coder::Free::MissCache>, one process-level cache that every backend and
every instance shares, so an address that is not in the data is searched for
once per process rather than once per call. Entries expire after an hour; this
method empties them now, for a caller that has rebuilt a database or downloaded
a data set while the process is running.
With a scope - C<OpenAddresses>, C<Local> or C<MaxMind> - only that backend's
entries go; C<OpenAddresses> covers every OpenAddresses directory this process
has opened. A scope that names no backend, such as
C<clear_miss_cache('Nonsense')>, forgets nothing and is not an error. Returns
true.
=head3 API SPECIFICATION
=head4 input
scope => { type => 'string', optional => 1 } # OpenAddresses, Local or MaxMind
=head4 output
{ type => 'boolean', value => 1 }
=cut | ||||||
| 1014 | |||||||
| 1015 | sub clear_miss_cache { | ||||||
| 1016 | # The invocant is $_[0] and is not needed; anything after it is the scope. | ||||||
| 1017 | 5 | 1 | 33 | Geo::Coder::Free::MissCache->clear(@_ > 1 ? $_[1] : undef); | |||
| 1018 | |||||||
| 1019 | 5 | 5 | return 1; | ||||
| 1020 | } | ||||||
| 1021 | |||||||
| 1022 - 1081 | =head2 provenance
my ($dist, $backend, $confidence, $accuracy) = $geocoder->provenance($point);
or, in scalar context, the same four as a hashref:
my $provenance = $geocoder->provenance($point);
print $provenance->{'backend'};
Where a result came from and how good it is, so that a caller does not have to
reach for the hash keys itself:
=over 4
=item * C<$dist> - the distribution that produced the result: the C<geocoder>
key, C<Geo::Coder::Free> for a result from this distribution, or the wrapper
that passed it on, such as C<Geo::Coder::List>.
=item * C<$backend> - which backend answered, in a short vocabulary: C<Local>,
C<MaxMind>, C<OpenAddresses> or C<GeoNames>. This is the C<database> key with
the distribution's prefix removed, so that the two spellings the backends use -
C<Geo::Coder::Free::Local> from C<Local>, and C<MaxMind> from C<MaxMind> - speak
the same vocabulary here. B<The C<database> key itself is unchanged>, for the
consumers that already match on it.
=item * C<$confidence> - how good the match was, 0.0 to 1.0, where 1.0 is exact.
=item * C<$accuracy> - how precise the coordinate is: C<address>, C<street>,
C<postcode>, C<locality>, C<city>, C<region> or C<country>.
=back
Returns an empty list - and an empty hashref in scalar context - when handed
something that is not a result. A key a result does not carry comes back
C<undef> rather than being guessed at, because a plain hash from another
geocoder has no provenance to report.
=head3 API SPECIFICATION
=head4 input
point => { type => 'hashref', optional => 1 } # a result, usually a Geo::Location::Point
=head4 output
# list context
{ type => 'array', min => 0, max => 4 } # dist, backend, confidence, accuracy; or ()
# scalar context
{
type => 'hashref',
schema => {
dist => { type => 'string', optional => 1 },
backend => { type => 'string', optional => 1 },
confidence => { type => 'number', optional => 1, min => 0, max => 1 },
accuracy => { type => 'string', optional => 1 },
},
} # {} when handed something that is not a result
=cut | ||||||
| 1082 | |||||||
| 1083 | sub provenance { | ||||||
| 1084 | 6 | 1 | 2330 | my ($self, $point) = @_; | |||
| 1085 | |||||||
| 1086 | 6 | 26 | return wantarray ? () : {} unless defined($point) && ref($point) | ||||
| 1087 | && ((Scalar::Util::reftype($point) || '') eq 'HASH'); | ||||||
| 1088 | |||||||
| 1089 | 4 | 6 | my $dist = $point->{'geocoder'}; | ||||
| 1090 | 4 | 4 | my $backend = $point->{'database'}; | ||||
| 1091 | 4 | 6 | $backend =~ s/^Geo::Coder::Free::// if defined($backend); | ||||
| 1092 | |||||||
| 1093 | my %provenance = ( | ||||||
| 1094 | dist => $dist, | ||||||
| 1095 | backend => $backend, | ||||||
| 1096 | confidence => $point->{'confidence'}, | ||||||
| 1097 | 4 | 12 | accuracy => $point->{'accuracy'}, | ||||
| 1098 | ); | ||||||
| 1099 | |||||||
| 1100 | 4 | 14 | return wantarray ? @provenance{qw(dist backend confidence accuracy)} : \%provenance; | ||||
| 1101 | } | ||||||
| 1102 | |||||||
| 1103 - 1131 | =head2 reverse_geocode
=head3 SYNOPSIS
my $loc = $geo->reverse_geocode(latlng => '51.3341,-1.4159');
=head3 DESCRIPTION
Translates a latitude/longitude pair back to a place name.
B<Partially implemented>: the MaxMind backend does not return meaningful results.
OpenAddresses is attempted first when available.
=head3 API SPECIFICATION
=head4 input
# Input schema (Params::Validate::Strict) - latlng required
latlng => { type => 'scalar' } # "$lat,$long" comma-separated decimal degrees
# NOTE: separate lat/lon/long keys are NOT supported at the Geo::Coder::Free
# (facade) level. When no OpenAddresses backend is configured, passing
# lat/lon/long instead of latlng will croak "not yet supported".
# To use separate coordinates call Geo::Coder::Free::Local::reverse_geocode.
=head4 output
# Output schema (Return::Set)
{ type => 'object', isa => 'Geo::Location::Point', optional => 1 }
=cut | ||||||
| 1132 | |||||||
| 1133 | sub reverse_geocode { | ||||||
| 1134 | 21 | 1 | 8780 | my $self = shift; | |||
| 1135 | |||||||
| 1136 | 21 | 38 | if (!ref($self)) { | ||||
| 1137 | 5 | 9 | if (scalar @_) { | ||||
| 1138 | 1 | 2 | return __PACKAGE__->new()->reverse_geocode(@_); | ||||
| 1139 | } elsif (!defined($self)) { | ||||||
| 1140 | 2 | 3 | Carp::croak(_i18n('usage_reverse', __PACKAGE__)); | ||||
| 1141 | } elsif ($self eq __PACKAGE__) { | ||||||
| 1142 | 1 | 1 | Carp::croak(_i18n('usage_reverse', $self)); | ||||
| 1143 | } | ||||||
| 1144 | 1 | 1 | return __PACKAGE__->new()->reverse_geocode($self); | ||||
| 1145 | } elsif (ref($self) eq 'HASH') { | ||||||
| 1146 | 0 | 0 | return __PACKAGE__->new()->reverse_geocode($self); | ||||
| 1147 | } | ||||||
| 1148 | |||||||
| 1149 | 16 | 19 | my %params = _normalize_args(_i18n('usage_reverse', __PACKAGE__), 'latlng', @_); | ||||
| 1150 | |||||||
| 1151 | # M8 (tautology - same pattern fixed in geocode): both arms are identical; | ||||||
| 1152 | # context propagates implicitly through return. | ||||||
| 1153 | 14 | 22 | if ($self->{'openaddr'}) { | ||||
| 1154 | 0 | 0 | return $self->{'openaddr'}->reverse_geocode(\%params); | ||||
| 1155 | } | ||||||
| 1156 | 14 | 20 | if ($params{'latlng'}) { | ||||
| 1157 | 5 | 10 | return $self->{'maxmind'}->reverse_geocode(\%params); | ||||
| 1158 | } | ||||||
| 1159 | |||||||
| 1160 | 9 | 12 | Carp::croak(_i18n('reverse_unsupported')); | ||||
| 1161 | } | ||||||
| 1162 | |||||||
| 1163 - 1179 | =head2 ua Does nothing. Present for drop-in compatibility with other Geo::Coder::* modules. =head3 API SPECIFICATION =head4 input # Input schema (Params::Validate::Strict) # any arguments are accepted and ignored =head4 output # Output schema (Return::Set) # nothing: an empty list, or undef in scalar context =cut | ||||||
| 1180 | |||||||
| 1181 | 1 | sub ua { } | |||||
| 1182 | |||||||
| 1183 - 1207 | =head2 run
Command-line entry point. Use as:
perl lib/Geo/Coder/Free.pm 1600 Pennsylvania Avenue NW, Washington DC
The arguments are joined with spaces into one location, which is geocoded with
OpenAddresses as well when C<OPENADDR_HOME> is set. The results are printed to
standard output with L<Data::Dumper>. It croaks C<geocoding failed> if nothing
is found.
=head3 API SPECIFICATION
=head4 input
# Input schema (Params::Validate::Strict)
class => { type => 'string' } # Geo::Coder::Free
location => { type => 'arrayref', of => 'string' } # the words of the location, joined with spaces
=head4 output
# Output schema (Return::Set)
{ type => 'boolean' } # the return value of print; croaks if nothing was found
=cut | ||||||
| 1208 | |||||||
| 1209 | __PACKAGE__->run(@ARGV) unless caller(); | ||||||
| 1210 | |||||||
| 1211 | sub run { | ||||||
| 1212 | 0 | 1 | 0 | require Data::Dumper; | |||
| 1213 | |||||||
| 1214 | 0 | 0 | my $class = shift; | ||||
| 1215 | 0 | 0 | my $location = join ' ', @_; | ||||
| 1216 | |||||||
| 1217 | my @rc = $ENV{'OPENADDR_HOME'} | ||||||
| 1218 | 0 | 0 | ? $class->new(openaddr => $ENV{'OPENADDR_HOME'})->geocode($location) | ||||
| 1219 | : $class->new()->geocode($location); | ||||||
| 1220 | |||||||
| 1221 | 0 | 0 | Carp::croak(_i18n('geocoding_failed', $0)) unless @rc; | ||||
| 1222 | |||||||
| 1223 | 0 | 0 | print Data::Dumper->new([\@rc])->Dump(); | ||||
| 1224 | } | ||||||
| 1225 | |||||||
| 1226 | # ----------------------------------------------------------------------- | ||||||
| 1227 | # Private helpers | ||||||
| 1228 | # ----------------------------------------------------------------------- | ||||||
| 1229 | |||||||
| 1230 | # The rule geocode() rejects on - whether a string could be a location at all - | ||||||
| 1231 | # is defined in Geo::Location::Predicate, and imported above. It is not | ||||||
| 1232 | # written out here as well: the backends ask the same question of what a caller | ||||||
| 1233 | # hands them, so one definition is what keeps valid_location(), the guard that | ||||||
| 1234 | # croaks and the backends from disagreeing about it. | ||||||
| 1235 | |||||||
| 1236 | # Purpose: Return a formatted message string from the message table. | ||||||
| 1237 | # Falls back to the raw key so call sites never die on a missing key. | ||||||
| 1238 | # Entry: $key - message key; @args - sprintf format arguments. | ||||||
| 1239 | # Exit: Formatted string. | ||||||
| 1240 | sub _i18n { | ||||||
| 1241 | 216 | 131740 | my ($key, @args) = @_; | ||||
| 1242 | 216 | 249 | my $fmt = $_MESSAGES{$key} // $key; | ||||
| 1243 | 216 | 913 | return @args ? sprintf($fmt, @args) : $fmt; | ||||
| 1244 | } | ||||||
| 1245 | |||||||
| 1246 | # Purpose: Normalise the four calling conventions accepted by geocode/reverse_geocode: | ||||||
| 1247 | # hashref, even-length key-val list, odd-length list, single bare string. | ||||||
| 1248 | # Entry: $error_msg - message to croak if a non-hash reference is passed. | ||||||
| 1249 | # $bare_key - hash key to assign to a single bare string argument | ||||||
| 1250 | # ('location' for geocode, 'latlng' for reverse_geocode). | ||||||
| 1251 | # @args - raw @_ after $self has been shifted. | ||||||
| 1252 | # Exit: Flat key-value %params hash. | ||||||
| 1253 | # Side Effects: None; croaks on unsupported reference argument. | ||||||
| 1254 | sub _normalize_args { | ||||||
| 1255 | 166 | 132209 | my ($error_msg, $bare_key, @args) = @_; | ||||
| 1256 | 166 11 | 175 18 | return %{$args[0]} if ref($args[0]) eq 'HASH'; | ||||
| 1257 | 155 | 195 | Carp::croak($error_msg) if ref($args[0]); | ||||
| 1258 | 147 | 384 | return @args if @args && @args % 2 == 0; | ||||
| 1259 | 30 | 43 | return ($bare_key => $args[0]) if @args == 1; | ||||
| 1260 | # M7 (logic-gap closure): odd-count > 1 is not a valid calling convention. | ||||||
| 1261 | # The former code silently returned () here, discarding the trailing elements. | ||||||
| 1262 | # Fail fast instead of propagating a malformed argument list downstream. | ||||||
| 1263 | 18 | 66 | Carp::croak($error_msg) if @args; | ||||
| 1264 | 11 | 13 | return (); | ||||
| 1265 | } | ||||||
| 1266 | |||||||
| 1267 | # Purpose: Try to geocode each candidate place string via the openaddr backend, | ||||||
| 1268 | # annotating every hit with location/text/confidence metadata and | ||||||
| 1269 | # memoising misses to avoid re-querying the same failing string. | ||||||
| 1270 | # Entry: $self - geocoder instance with {openaddr} and {scantext_misses}. | ||||||
| 1271 | # $candidates - arrayref of place-name strings to probe. | ||||||
| 1272 | # $region - optional ISO country code appended to each candidate. | ||||||
| 1273 | # $confidence - numeric confidence score assigned to all hits. | ||||||
| 1274 | # $scantext - original free text (stored inside each result object). | ||||||
| 1275 | # Exit: Arrayref of Geo::Location::Point objects; empty arrayref on no hits. | ||||||
| 1276 | # Side Effects: Populates $self->{scantext_misses} for every non-matching place. | ||||||
| 1277 | sub _resolve_scan_candidates { | ||||||
| 1278 | 25 | 4608 | my ($self, $candidates, $region, $confidence, $scantext) = @_; | ||||
| 1279 | 25 | 14 | my @results; | ||||
| 1280 | 25 25 | 15 18 | for my $place (@{$candidates}) { | ||||
| 1281 | 33 | 62 | my $location = $region ? "$place, $region" : $place; | ||||
| 1282 | 33 | 33 | next if $self->{'scantext_misses'}{$location}; | ||||
| 1283 | 32 30 | 32 86 | my @res = grep { defined } $self->{'openaddr'}->geocode($location); | ||||
| 1284 | 32 | 28 | if (@res) { | ||||
| 1285 | 1 | 1 | for my $entry (@res) { | ||||
| 1286 | 1 | 14 | $entry->{'location'} = $location; | ||||
| 1287 | 1 | 1 | $entry->{'text'} = $scantext; | ||||
| 1288 | 1 | 1 | $entry->{'confidence'} = $confidence; | ||||
| 1289 | } | ||||||
| 1290 | 1 | 1 | push @results, @res; | ||||
| 1291 | } else { | ||||||
| 1292 | 31 | 34 | $self->{'scantext_misses'}{$location} = 1; | ||||
| 1293 | } | ||||||
| 1294 | } | ||||||
| 1295 | 25 | 25 | return \@results; | ||||
| 1296 | } | ||||||
| 1297 | |||||||
| 1298 | # Purpose: Slide a window of $n words across $text and return all comma-joined | ||||||
| 1299 | # n-grams, excluding pure-numeric tokens and stopwords. A token of | ||||||
| 1300 | # digits only is not a word of a place name, which is the question | ||||||
| 1301 | # Geo::Location::Predicate::_valid_location() answers, so it is asked | ||||||
| 1302 | # there rather than spelled out here (t/location-rule.t fails if the | ||||||
| 1303 | # rule is written out again). | ||||||
| 1304 | # Replaces the former _find_word_triplets ($n=3) and | ||||||
| 1305 | # _find_word_duplets ($n=2) which were identical except for window size | ||||||
| 1306 | # and the duplets version was missing the lc() normalisation on stopwords. | ||||||
| 1307 | # Entry: $text - raw string; $n - window size; $stop - stopword hashref. | ||||||
| 1308 | # Exit: Flat list of n-gram strings. | ||||||
| 1309 | sub _find_word_ngrams { | ||||||
| 1310 | 33 | 17064 | my ($text, $n, $stop) = @_; | ||||
| 1311 | 33 | 36 | $text =~ s/,+/ /g; | ||||
| 1312 | 33 | 77 | $text =~ s/\s+/ /g; | ||||
| 1313 | 33 | 83 | $text =~ s/^\s+|\s+$//g; | ||||
| 1314 | 33 119 | 75 278 | my @words = grep { !$stop->{lc $_} && _valid_location($_) } split /\s+/, $text; | ||||
| 1315 | 33 | 107 | my @ngrams; | ||||
| 1316 | 33 | 42 | for my $i (0 .. $#words - ($n - 1)) { | ||||
| 1317 | 51 | 86 | push @ngrams, join ', ', @words[$i .. $i + $n - 1]; | ||||
| 1318 | } | ||||||
| 1319 | 33 | 41 | return @ngrams; | ||||
| 1320 | } | ||||||
| 1321 | |||||||
| 1322 | # Purpose: Extract structured US street addresses from free text. | ||||||
| 1323 | # Entry: $text - arbitrary string. | ||||||
| 1324 | # Exit: List of full address strings matching the US pattern. | ||||||
| 1325 | sub _find_us_addresses { | ||||||
| 1326 | 4 | 3573 | my $text = shift; | ||||
| 1327 | 4 | 4 | my @addresses; | ||||
| 1328 | 4 | 6 | my $re = qr/ | ||||
| 1329 | \b \d{1,5} \s+ # house number | ||||||
| 1330 | (?:[A-Za-z0-9]+(?:\s+[A-Za-z0-9]+){0,6}) \s+ # street name: 1-7 words (bounded) | ||||||
| 1331 | (?:Avenue|Ave\.?|Boulevard|Blvd\.?|Road|Rd\.?|Lane|Ln\.?|Drive|Dr\.?|Street|St\.?) | ||||||
| 1332 | (?:\s+[A-Za-z]{2})? ,\s* | ||||||
| 1333 | (?:[A-Za-z]+(?:\s+[A-Za-z]+){0,3}) ,\s* # city: 1-4 words | ||||||
| 1334 | [A-Z]{2} \s* (?:\d{5}(?:-\d{4})?)? \b # state + optional zip | ||||||
| 1335 | /x; | ||||||
| 1336 | 4 | 37 | while ($text =~ /$re/g) { | ||||
| 1337 | 2 | 7 | push @addresses, $&; | ||||
| 1338 | } | ||||||
| 1339 | 4 | 6 | return @addresses; | ||||
| 1340 | } | ||||||
| 1341 | |||||||
| 1342 | # Purpose: Extract British-style addresses from free text. | ||||||
| 1343 | # Entry: $text - arbitrary string. | ||||||
| 1344 | # Exit: List of trimmed address strings. | ||||||
| 1345 | sub _find_gb_addresses { | ||||||
| 1346 | 5 | 4610 | my $text = shift; | ||||
| 1347 | 5 | 5 | my @addresses; | ||||
| 1348 | # ReDoS fix: the former pattern used \s*,?\s* between all parts and | ||||||
| 1349 | # [A-Za-z\s'-]+ groups - with commas optional, the engine must try all | ||||||
| 1350 | # possible splits of a long string among 5 overlapping space-containing | ||||||
| 1351 | # groups, causing exponential backtracking. Making commas mandatory | ||||||
| 1352 | # eliminates the ambiguity entirely: UK postal addresses always have commas. | ||||||
| 1353 | # All groups are non-capturing (only $& is used). | ||||||
| 1354 | 5 | 8 | my $re = qr/ | ||||
| 1355 | \b | ||||||
| 1356 | (?:\d{1,5} | [\w'-]+) # house number or single-word name | ||||||
| 1357 | \s+ | ||||||
| 1358 | (?:[\w'-]+(?:\s+[\w'-]+){0,5}) # street name (up to 6 words) | ||||||
| 1359 | \s*,\s* # COMMA REQUIRED - kills the ambiguity | ||||||
| 1360 | (?:[\w'-]+(?:\s+[\w'-]+){0,3}) # town (up to 4 words) | ||||||
| 1361 | \s*,\s* # COMMA REQUIRED | ||||||
| 1362 | (?:[\w'-]+(?:\s+[\w'-]+){0,3}) # county (up to 4 words) | ||||||
| 1363 | \s*,\s* # COMMA REQUIRED | ||||||
| 1364 | (?:[\w'-]+(?:\s+[\w'-]+){0,2}) # country (up to 3 words) | ||||||
| 1365 | \b | ||||||
| 1366 | /x; | ||||||
| 1367 | 5 | 53 | while ($text =~ /$re/g) { | ||||
| 1368 | 1 | 4 | (my $addr = $&) =~ s/[,\s]+$//; | ||||
| 1369 | 1 | 2 | push @addresses, $addr; | ||||
| 1370 | } | ||||||
| 1371 | 5 | 10 | return @addresses; | ||||
| 1372 | } | ||||||
| 1373 | |||||||
| 1374 | # Purpose: Extract Canadian street addresses from free text. | ||||||
| 1375 | # Entry: $text - arbitrary string. | ||||||
| 1376 | # Exit: List of full address strings matching the Canadian pattern. | ||||||
| 1377 | sub _find_ca_addresses { | ||||||
| 1378 | 3 | 2465 | my $text = shift; | ||||
| 1379 | 3 | 4 | my @addresses; | ||||
| 1380 | 3 | 4 | my $re = qr/ | ||||
| 1381 | \b \d{1,5} \s+ # house number | ||||||
| 1382 | (?:[A-Za-z0-9]+(?:\s+[A-Za-z0-9]+){0,6}) \s+ # street name: 1-7 words (bounded) | ||||||
| 1383 | (?:Avenue|Ave\.?|Boulevard|Blvd\.?|Road|Rd\.?|Lane|Ln\.?|Drive|Dr\.?|Street|St\.?|Circle|Crescent|Cres\.?) | ||||||
| 1384 | \s*,\s* | ||||||
| 1385 | (?:[A-Za-z]+(?:\s+[A-Za-z]+){0,3}) \s*,\s* # city: 1-4 words | ||||||
| 1386 | [A-Z]{2} \s*,?\s* | ||||||
| 1387 | (?:[A-Z]\d[A-Z]\s?\d[A-Z]\d)? \b # optional postal code | ||||||
| 1388 | /x; | ||||||
| 1389 | 3 | 22 | while ($text =~ /$re/g) { | ||||
| 1390 | 2 | 7 | push @addresses, $&; | ||||
| 1391 | } | ||||||
| 1392 | 3 | 8 | return @addresses; | ||||
| 1393 | } | ||||||
| 1394 | |||||||
| 1395 | # _normalize and _abbreviate are imported from Geo::Coder::Free::Utils. | ||||||
| 1396 | |||||||
| 1397 - 1558 | =head1 GETTING STARTED
To download, import and set up the local database:
before running C<make>, but after running C<perl Makefile.PL>, follow these instructions.
Optionally set C<OPENADDR_HOME> to point to an empty directory and download the data from
L<http://results.openaddresses.io> into that directory; and
optionally set C<WHOSONFIRST_HOME> to point to an empty directory and download the data using
L<https://github.com/nigelhorne/NJH-Snippets/blob/master/bin/wof-clone>.
The script C<bin/download_databases> (see below) will do those for you.
You do not need to download the MaxMind data - that is downloaded automatically.
You will need to create the database used by C<Geo::Coder::Free>.
Install L<App::csv2sqlite> and L<https://github.com/nigelhorne/NJH-Snippets>.
Run C<bin/create_sqlite> - this converts the MaxMind "cities" database from CSV to SQLite.
To use with MariaDB, set C<MARIADB_SERVER="$hostname;$port"> and
C<MARIADB_USER="$user;$password"> (TODO: username/password should be asked for interactively).
The code will use a database called C<geo_code_free>, which will be dropped and recreated if it exists.
C<$user> needs only DROP, CREATE, SELECT, INSERT, and INDEX privileges on that database.
The following optional steps download and install large databases.
This will take a long time and use a lot of disc space.
=over 4
=item 1
C<mkdir $WHOSONFIRST_HOME; cd $WHOSONFIRST_HOME> then run C<wof-clone> from NJH-Snippets.
This can take a long time because it contains many nested directories, which filesystem drivers
can be slow to navigate (particularly on EXT4 and ZFS).
=item 2
Install L<https://github.com/dr5hn/countries-states-cities-database.git> into C<$DR5HN_HOME>.
This data covers cities only, so it is not used when C<OSM_HOME> is set (OSM is far more
comprehensive). Only Australia, Canada, and the US are imported, as the UK data is difficult
to parse.
=item 3
Run C<bin/download_databases> - this downloads the Who's On First, OpenAddr, OpenStreetMap,
and dr5hn databases.
OpenStreetMap now uses PBF files, so you will need C<apt install osmium-tool> first.
Check the values of C<OSM_HOME>, C<OPENADDR_HOME>, C<DR5HN_HOME> and C<WHOSONFIRST_HOME>
within that script and adjust them for your setup.
The C<Makefile.PL> file downloads the MaxMind database automatically, as it is not optional.
=item 4
Run C<bin/create_db> - this creates the database used by C<Geo::Coder::Free> from the data you
have just downloaded.
The database is called C<openaddr.sql> for historical reasons (before Who's On First was added);
it actually contains data from all sources above.
=back
Now you are ready to run C<make>.
See the comment at the start of C<createdatabase.PL> for further details.
=head1 MORE INFORMATION
I have written several Perl genealogy programs including
L<gedcom|https://github.com/nigelhorne/gedcom> and
L<ged2site|https://github.com/nigelhorne/ged2site>.
One of the things these do is check the validity of a family tree, including verifying place-names.
Of course places do change names and spelling becomes more consistent over the years, but the vast
majority remain the same - enough to make computerised verification worthwhile.
=head1 BUGS
Some lookups fail. Please file a bug report at
L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Geo-Coder-Free>.
The MaxMind data contains cities only.
The OpenAddresses data does not cover the whole globe.
C<London, England> cannot be parsed yet.
=head1 SEE ALSO
=over 4
=item * L<Configure an Object at Runtime|Object::Configure>
=item * L<Test Dashboard|https://nigelhorne.github.io/Geo-Coder-Free/coverage/>
=back
L<Geo::Coder::Free::Local>, L<Geo::Coder::Free::MaxMind>,
L<Geo::Coder::Free::OpenAddresses>,
L<https://openaddresses.io/>, L<https://www.maxmind.com/>,
L<https://www.geonames.org/>, L<https://www.whosonfirst.org/>.
=head1 AUTHOR
Nigel Horne C<< <njh@nigelhorne.com> >>
=head1 FORMAL SPECIFICATION
=head2 new
GeoCoderFreeState ::= â¨â¨ maxmind : MaxMind_Geocoder;
openaddr : OpenAddr_Geocoder | undef;
alternatives: Map[STRING â STRING];
cache : Cache | undef â©â©
OpenAddrState ::= â¨â¨ openaddr : PATH;
cache : Cache | undef;
libpostal : BOOLEAN â©â©
Init : Params â GeoCoderFreeState
â p : Params â¢
let oa_path == p.openaddr ⨠env.OPENADDR_HOME â¢
GeoCoderFreeState.openaddr = if oa_path â â
then OpenAddresses(oa_path, p.cache, p.libpostal)
else undef fi
â§ (oa_path â â
â¹ GeoCoderFreeState.openaddr.libpostal =
if p.libpostal = undef then true else p.libpostal fi)
-- libpostal = false: Geo::libpostal is never loaded or called
â s : OpenAddrState ⢠¬ s.libpostal â¹ Geo::libpostal â loaded_by(s.geocode)
=head2 valid_location
ValidLocation : Any â BOOLEAN
â x : Any â¢
ValidLocation(x) = (x â â
⧠¬ ref(x) â§ x â "" â§ x â non-digit)
=head2 geocode
ErrorMode ::= "croak" | "undef"
Geocode : Address à Region? à ErrorMode? â Point?
â addr : Address; r : Region?; e : ErrorMode? â¢
let mode == e ⨠"croak" â¢
let backends == (openaddr â undef â¹ [OpenAddresses, Local, MaxMind])
â§ (openaddr = undef â¹ [MaxMind]) â¢
¬ ValidLocation(addr) â¹ (mode = "undef" â¹ result = â
| mode = "croak" â¹ â¥)
â§ ValidLocation(addr) â¹
result = first { defined } map { b.geocode(addr, r) } backends
=head2 provenance
Provenance : Point? â (DISTRIBUTION?, BACKEND?, CONFIDENCE?, ACCURACY?)
â p : Point? â¢
Provenance(p) = if p â â
â§ is_a_result(p)
then â¨â¨ p.geocoder,
strip("Geo::Coder::Free::", p.database),
p.confidence,
p.accuracy â©â©
else â
fi
=head1 LICENSE AND COPYRIGHT
Copyright 2017-2026 Nigel Horne. Licensed under GPL2 for personal use.
This product uses GeoLite2 data created by MaxMind,
available from L<https://www.maxmind.com/>.
=cut | ||||||
| 1559 | |||||||
| 1560 | 1; | ||||||
| 1561 | |||||||
| 1562 | # Common mappings for looser lookups, one "from = to" per line; lines starting | ||||||
| 1563 | # with # are ignored. They are tried only after every backend has missed, so | ||||||
| 1564 | # "Tyne and Wear" is a last try under the county GeoNames uses, not a claim | ||||||
| 1565 | # that all of Tyne and Wear is in North Tyneside. A future version should load | ||||||
| 1566 | # these from an external, user-editable config file. See also Local.pm's | ||||||
| 1567 | # %alternatives. | ||||||