Name

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

Version

Version 0.39

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();

Description

Geo::Coder::All and Geo::Coder::Many are great modules but neither quite does what I want.

A geocoder turns the name of a place, such as '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.

Geo::Coder::List lets you use many geocoders as if they were one:

Supported Answer Formats

Each geocoding service answers in its own format. 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 first.

Some services need special handling, which is done for you:

Every answer that went through this process also has a 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.

Subroutines/Methods

New

Creates a new Geo::Coder::List object with no geocoders in it.

Settings can also come from the environment, through Object::Configure. The variable name is GEO__CODER__LIST__ followed by the setting name. For example, GEO__CODER__LIST__carp_on_warn=1 makes warnings go through Carp, and GEO__CODER__LIST__cache_hit_duration='7 days' changes how long a found place stays in L2.

When you call new() on an object that already exists, you get a 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 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.

If new is called as a function, Geo::Coder::List::new(), with no arguments, it still returns an object. Called as a function with arguments, it warns and returns undef.

Api Specification

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 },
}
Output
{
    type => 'object',
    isa  => 'Geo::Coder::List',
}

# undef only when called as Geo::Coder::List::new() with arguments

Push

Adds one geocoder to the end of the chain. Geocoders are asked in the order in which they were added.

push returns the object itself, so you can add several geocoders in one statement.

There are two ways to add a geocoder:

my $list = Geo::Coder::List->new()
    ->push({ regex => qr/USA$/, geocoder => Geo::Coder::US::Census->new(), limit => 100 })
    ->push(Geo::Coder::OSM->new());

push croaks if $geocoder is missing or undef.

Api Specification

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)
Output
{
    type => 'object',
    isa  => 'Geo::Coder::List',
}

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:

Scalar context returns one hash reference, or undef.

List context returns every answer from the geocoder that won, or an empty list. This is useful when a name, such as '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 geometry before you use each one.

my @all = $list->geocode('Springfield, USA');

The geocoder key of an answer holds the geocoder object that gave it. When the answer came from the cache, it holds the string 'cache'.

Api Specification

Input
{
    location => { type => 'string', optional => 0, min => 1 },
}
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

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 $ua has both clone() and agent() methods, as LWP::UserAgent does, each geocoder is given its own copy. The agent string of each copy is set to the geocoder's class name and version, such as 'Geo::Coder::OSM/0.03', so each service can see which module is calling it. Otherwise every geocoder is given $ua itself.

Add the geocoders before you call 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 $ua is missing or false, nothing changes and nothing is returned. ua croaks with No geocoder found if an entry in the chain has no geocoder.

Api Specification

Input
{
    ua => { type => 'object', optional => 1 },
}
Output
{
    type => 'object',
    optional => 1,
}

# The same $ua that was passed in, or nothing when $ua is false

Reverse_Geocode

Finds the address of a position: the opposite of 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');

latlng is the latitude, a comma, and the longitude, in that order. It must be two plain decimal numbers written with the digits 0 to 9. Spaces, tabs and a + or - sign are allowed. Anything else, such as '51.5;-0.1', '1e5,2' or '51.5', makes reverse_geocode croak before any geocoder is asked.

The geocoders are asked in turn, as for geocode. limit is honoured; regex is not. If a geocoder complains that it does not know the latlng parameter, it is asked again with only lat and lon. If a geocoder dies for any other reason, a warning is given and the next geocoder is asked.

Scalar context returns one address string, or undef.

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:

If a geocoder returns a plain string, the string is the address.

Addresses are cached under the latlng string, so asking again for the same position does not ask any geocoder.

Api Specification

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]*$/ },
}
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

Log

Returns the record of what has happened since the object was made, or since the last flush(). There is one entry for each question asked of a geocoder, and one for each answer that geocode() serves from the cache. 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:

The returned array reference is the object's own record, not a copy.

Api Specification

Input
[]
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
#   }

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');

flush does not empty the cache.

Api Specification

Input
[]
Output
{
    type => 'object',
    isa  => 'Geo::Coder::List',
}

Common Pitfalls

Encoding

Author

Nigel Horne, <njh at nigelhorne.com>

Bugs

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:

See Also

Support

You can find documentation for this module with the perldoc command:

perldoc Geo::Coder::List

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.

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

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′ = ⟨⟩
└────────────────────────────────────────────────────────────────

Push

┌─ Push ─────────────────────────────────────────────────────────
│ ΔGeoCoderList
│ g? : Entry
├────────────────────────────────────────────────────────────────
│ g? ≠ ⊥
│ geocoders′ = geocoders ⁀ ⟨g?⟩
│ L1′ = L1 ∧ L2′ = L2 ∧ log′ = log
└────────────────────────────────────────────────────────────────

PushUndef ≙ [ ΞGeoCoderList ; g? : Entry | g? = ⊥ ] ⇒ croak

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 ⟩
└────────────────────────────────────────────────────────────────

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)
└────────────────────────────────────────────────────────────────

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)
└────────────────────────────────────────────────────────────────

Log

┌─ Log ──────────────────────────────────────────────────────────
│ ΞGeoCoderList
│ entries! : seq LogEntry
├────────────────────────────────────────────────────────────────
│ entries! = log
└────────────────────────────────────────────────────────────────

Flush

┌─ Flush ────────────────────────────────────────────────────────
│ ΔGeoCoderList
│ self! : GeoCoderList
├────────────────────────────────────────────────────────────────
│ log′ = ⟨⟩
│ geocoders′ = geocoders ∧ L1′ = L1 ∧ L2′ = L2
│ self! = θGeoCoderList′
└────────────────────────────────────────────────────────────────

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.

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)|
                                 +--------------+

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.

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

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

Copyright 2016-2026 Nigel Horne.

Usage is subject to the GPL2 licence terms. If you use it, please let me know.