Geo::Coder::List - Call many Geo-Coders
Version 0.37
Geo::Coder::All and Geo::Coder::Many are great modules but neither quite does what I want.
Geo::Coder::List aggregates multiple geocoding services into a single,
unified interface. It chains and prioritizes backends based on regex routing
and per-geocoder query limits, caches results at two levels (L1 in-memory
always; optional L2 via CHI or a plain HASH), and normalizes every provider's
idiosyncratic response into the common structure expected by
HTML::GoogleMaps::V3 and HTML::OSM:
$result->{geometry}{location}{lat} # canonical latitude
$result->{geometry}{location}{lng} # canonical longitude
$result->{geocoder} # source object (or 'cache')
use Geo::Coder::List;
use Geo::Coder::OSM;
use Geo::Coder::CA;
my $list = Geo::Coder::List->new()
->push({ regex => qr/(Canada|USA)$/, geocoder => Geo::Coder::CA->new() })
->push(Geo::Coder::OSM->new());
my $loc = $list->geocode('10 Downing St, London, UK');
printf "lat=%.4f lng=%.4f\n",
$loc->{geometry}{location}{lat},
$loc->{geometry}{location}{lng};
Creates a new Geo::Coder::List object. When called on an existing object
it returns a clone of that object merged with the supplied arguments.
The constructor reads configuration from environment variables via
Object::Configure; for example, setting
GEO__CODER__LIST__carp_on_warn=1 causes warnings to use Carp.
use Geo::Coder::List;
use CHI;
# With an optional L2 cache (any CHI driver works)
my $geocoder = Geo::Coder::List->new(
cache => CHI->new(driver => 'Memory', global => 1),
debug => 0,
);
# Clone an existing object with a higher debug level
my $verbose = $geocoder->new(debug => 2);
# Params::Validate::Strict schema
{
cache => {
type => [ 'hashref', 'object' ], # OBJECT must implement get($key) and set($key, $value, $ttl)
optional => 1,
},
debug => {
type => 'boolean',
optional => 1,
default => 0,
},
# Any additional key is forwarded to Object::Configure
}
# Return::Set schema
OBJECT blessed into Geo::Coder::List
Appends a geocoder to the chain. Geocoders are tried in the order they
were pushed. Returns $self so calls can be chained.
A plain geocoder object is tried for every location. A hashref with
regex, geocoder, and optional limit keys restricts the geocoder to
locations matching the regex and caps total queries at limit.
my $list = Geo::Coder::List->new()
->push({ regex => qr/USA$/, geocoder => Geo::Coder::CA->new(), limit => 100 })
->push(Geo::Coder::OSM->new());
# Params::Validate::Strict schema
{
geocoder => {
type => OBJECT | HASHREF,
required => 1,
# HASHREF must contain: geocoder => OBJECT
# HASHREF may contain: regex => Regexp
# limit => SCALAR (positive integer)
},
}
# Return::Set schema
OBJECT blessed into Geo::Coder::List # $self, for chaining
Resolves a location string to geographic coordinates by trying each geocoder in turn. The first successful result is returned and cached.
In scalar context returns a single hashref (or undef on failure).
In list context returns all results from the winning geocoder.
The geocoder field of the returned hashref holds the geocoder object that
supplied the result; it is set to the string 'cache' when the result was
served from cache.
See Geo::Coder::GooglePlaces::V3 for the canonical result structure.
my $result = $list->geocode(location => 'Paris, France');
if($result) {
printf "lat=%.4f lng=%.4f via %s\n",
$result->{geometry}{location}{lat},
$result->{geometry}{location}{lng},
ref($result->{geocoder}) || $result->{geocoder};
}
# List context returns all candidates from the winning geocoder
my @results = $list->geocode('London, UK');
# Params::Validate::Strict schema
{
location => {
type => SCALAR,
required => 1,
# Must contain at least one non-digit character
},
}
# Return::Set schema (scalar context)
HASHREF | undef
{
geometry => { location => { lat => Num, lng => Num } },
geocoder => OBJECT | 'cache',
lat => Num, # convenience alias
lng => Num, # convenience alias
lon => Num, # compatibility alias for lng
debug => Int, # source line of the normalisation branch taken
# ... provider-specific keys are preserved
}
# Return::Set schema (list context)
ARRAY of the above HASHREFs
Sets the LWP::UserAgent (or compatible) object on every geocoder in the chain. Useful when you need proxy support or custom timeouts across all backends at once.
There is intentionally no read accessor since that would be meaningless (each geocoder could have a different UA).
use LWP::UserAgent;
my $ua = LWP::UserAgent->new();
$ua->env_proxy(1);
$list->ua($ua);
# Params::Validate::Strict schema
{
ua => {
type => OBJECT,
optional => 1,
},
}
# Return::Set schema
OBJECT # the same $ua that was passed in
Converts a latitude/longitude pair into a human-readable address string.
In scalar context returns a single address string (or undef).
In list context returns all address strings from the winning geocoder.
my $address = $list->reverse_geocode(latlng => '51.5074,-0.1278');
print "Address: $address\n" if $address;
my @addresses = $list->reverse_geocode(latlng => '51.5074,-0.1278');
# Params::Validate::Strict schema
{
latlng => {
type => SCALAR,
required => 1,
regex => qr/^\s*[-+]?(?:\d*\.?\d+|\d+\.?\d*)
\s*,\s*
[-+]?(?:\d*\.?\d+|\d+\.?\d*)\s*$/x,
},
}
# Return::Set schema (scalar context)
SCALAR (address string) | undef
# Return::Set schema (list context)
ARRAY of SCALAR
Returns an arrayref of log entries accumulated since the last flush().
Each entry is a hashref with the keys: line, location, timetaken,
geocoder, wantarray, and either result or error.
foreach my $entry (@{ $list->log() }) {
printf "%s: %.3fs via %s\n",
$entry->{location},
$entry->{timetaken},
$entry->{geocoder};
}
# No parameters accepted
# Return::Set schema
ARRAYREF of HASHREF
[
{
line => Int,
location => Str,
timetaken => Num,
geocoder => Str | 'cache',
wantarray => Bool,
result => HASHREF | ARRAYREF | Str, # on success
error => Str, # on failure
},
...
]
Clears all accumulated log entries and returns $self to allow chaining.
$list->geocode('Paris, France');
my $entries = $list->log();
$list->flush()->geocode('London, UK'); # chained
# No parameters accepted
# Return::Set schema
OBJECT blessed into Geo::Coder::List # $self, for chaining
Nigel Horne, <njh at nigelhorne.com>
Please report any bugs or feature requests to
bug-geo-coder-list at rt.cpan.org, or through the web interface at
https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Geo-Coder-List.
Known limitations:
reverse_geocode()does not yet support Geo::Location::Point objects.- When
Geo::GeoNamesreturns multiple candidates, only the first element of each sub-array is considered.
- Test Dashboard
- Geo::Coder::All
- Geo::Coder::GooglePlaces
- Geo::Coder::Many
- Configure an Object at Runtime
- Readonly
You can find documentation for this module with the perldoc command:
perldoc Geo::Coder::List
-
RT: CPAN's request tracker
-
MetaCPAN
List_State
──────────────────────────────────────────────────────
geocoders : seq (Geocoder | RegexGeocoder)
L1 : LocationStr ↛ (GeoResult | NotFound)
log : seq LogEntry
debug : ℕ
cache? : L2Cache
new
──────────────────────────────────────────────────────
List_State
params? : ℙ(Key × Value)
──────────────────────────────────────────────────────
geocoders = ⟨⟩
L1 = ∅
log = ⟨⟩
debug = params?.debug ∣ DEBUG_DEFAULT
cache = params?.cache ∣ ⊥
push
──────────────────────────────────────────────────────
ΔList_State
g? : Geocoder | RegexGeocoder
──────────────────────────────────────────────────────
geocoders' = geocoders ⌢ ⟨g?⟩
L1' = L1
log' = log
──────────────────────────────────────────────────────
where RegexGeocoder ::= { regex : Regex
; geocoder : Geocoder
; limit? : ℕ }
LocationStr ::= { s : seq Char | s ≠ ⟨⟩ ∧ ∃ c : s • c ∉ Digit }
GeoResult ::= HASHREF with geometry.location.{lat,lng} : ℝ
geocode
──────────────────────────────────────────────────────────────────────
ΔList_State
loc? : LocationStr
result! : GeoResult | ⊥
──────────────────────────────────────────────────────────────────────
loc? ∈ dom L1
⟹ result! = L1(loc?)
∧ log' = log ⌢ ⟨{geocoder ↦ cache; timetaken ↦ 0}⟩
loc? ∉ dom L1
⟹ (∃ i : 1..#geocoders •
applies(geocoders i, loc?)
∧ result! = Normalize(geocoders i . geocode(loc?))
∧ L1' = L1 ⊕ {loc? ↦ result!}
∧ log' = log ⌢ ⟨{geocoder ↦ class(geocoders i)}⟩)
∨ (result! = ⊥ ∧ L1' = L1 ⊕ {loc? ↦ ⊥})
applies(g, loc) ≙
(g isa Geocoder)
∨ (g isa RegexGeocoder ∧ loc ∈ matches(g.regex) ∧ g.limit > 0)
ua
──────────────────────────────────────────────────────
ΞList_State
ua? : UserAgent
ua! : UserAgent
──────────────────────────────────────────────────────
∀ g : ran geocoders • g.ua = ua?
ua! = ua?
LatLngStr ::= { s : seq Char
| s matches /^[-+]?\d+\.?\d*,[-+]?\d+\.?\d*$/ }
reverse_geocode
──────────────────────────────────────────────────────────────────────
ΔList_State
latlng? : LatLngStr
result! : seq Char | ⊥
──────────────────────────────────────────────────────────────────────
latlng? ∈ dom L1
⟹ result! = L1(latlng?)
latlng? ∉ dom L1
⟹ (∃ i : 1..#geocoders •
applies(geocoders i, latlng?)
∧ result! = geocoders i . reverse_geocode(latlng?)
∧ L1' = L1 ⊕ {latlng? ↦ result!})
∨ result! = ⊥
log
──────────────────────────────────────────────────────
ΞList_State
result! : seq LogEntry
──────────────────────────────────────────────────────
result! = log
flush
──────────────────────────────────────────────────────
ΔList_State
──────────────────────────────────────────────────────
log' = ⟨⟩
geocoders' = geocoders
L1' = L1
Copyright 2016-2026 Nigel Horne.
Usage is subject to the GPL2 licence terms. If you use it, please let me know.