package Geo::Coder::OpenCage;
# ABSTRACT: Geocode coordinates and addresses with the OpenCage Geocoder
$Geo::Coder::OpenCage::VERSION = '0.28';
use strict;
use warnings;

use Carp;
use HTTP::Tiny;
use JSON::MaybeXS;
use URI;
# FIXME - must be a way to get this from dist.ini?
my $version = 0.28;
my $ua_string;

sub new {
    my $class = shift;
    my %params = @_;

    if (!$params{api_key}) {
        croak "api_key is a required parameter for new()";

    $ua_string = $class . ' ' . $version;
    my $ua = $params{ua} || HTTP::Tiny->new(agent => $ua_string);
    my $self = {
        version => $version,
        api_key => $params{api_key},
        ua      => $ua,
        json    => JSON::MaybeXS->new( utf8 => 1 ),
        url     => URI->new(''),

    return bless $self, $class;

sub ua {
    my $self = shift;
    my $ua   = shift;
    if (defined($ua)){
        $self->{ua} = $ua;
    return $self->{ua};

# see list:
my %valid_params = (
    abbrv            => 1,
    add_request      => 1,
    bounds           => 1,
    countrycode      => 1,
    format           => 0,
    jsonp            => 0,
    language         => 1,
    limit            => 1,
    min_confidence   => 1,
    no_annotations   => 1,
    no_dedupe        => 1,
    no_record        => 1,
    q                => 1,
    pretty           => 1,  # makes no actual difference
    proximity        => 1,
    roadinfo         => 1,

sub geocode {
    my $self = shift;
    my %params = @_;

    if (defined($params{location})) {
        $params{q} = delete $params{location};
    else {
        warn "location is a required parameter for geocode()";
        return undef;

    for my $k (keys %params){
        if (!defined($params{$k})){
            warn "Unknown geocode parameter: $k";
            delete $params{$k};
        if (!$params{$k}){  # is a real parameter but we dont support it
            warn "Unsupported geocode parameter: $k";
            delete $params{$k};

    my $URL = $self->{url}->clone();
        key => $self->{api_key},

    my $response = $self->{ua}->get($URL);

    if (!$response){
        warn "failed to fetch '$URL': ", $response->{reason};
        return undef;

    my $rh_content = $self->{json}->decode( $response->{content} );

    if (!$response->{success}) {
        warn "response when requesting '$URL': "
            . $rh_content->{status}{code}
            . ', '
            . $rh_content->{status}{message};
        return undef;
    return $rh_content;

sub reverse_geocode {
    my $self = shift;
    my %params = @_;

    foreach my $k (qw(lat lng)){
        if (!defined($params{$k})){
            warn "$k is a required parameter";
            return undef;

    $params{location} = join(',', delete @params{'lat','lng'});
    return $self->geocode(%params);




=encoding UTF-8

=head1 NAME

Geo::Coder::OpenCage - Geocode coordinates and addresses with the OpenCage Geocoder

=head1 VERSION

version 0.28


    my $Geocoder = Geo::Coder::OpenCage->new(api_key => $my_api_key);

    my $result = $Geocoder->geocode(location => "Donostia");


This module provides an interface to the OpenCage geocoding service.

For full details of the API visit L<>.

It is recommended you read the L<best practices for using the OpenCage geocoder|> before you start.

=head1 METHODS

=head2 new

    my $Geocoder = Geo::Coder::OpenCage->new(api_key => $my_api_key);

Get your API key from L<>

=head2 ua

    $ua = $geocoder->ua();
    $ua = $geocoder->ua($ua);

Accessor for the UserAgent object. By default HTTP::Tiny is used. Useful if for
example you want to specify that something like LWP::UserAgent::Throttled for 
rate limiting. Even if a new UserAgent is specified the useragent string will 
be specified as "Geo::Coder::OpenCage $version"

=head2 geocode

Takes a single named parameter 'location' and returns a result hashref.

    my $result = $Geocoder->geocode(location => "Mudgee, Australia");

warns and returns undef if the query fails for some reason.

If you will be doing forward geocoding, please see the 
L<OpenCage query formatting guidelines|>

The OpenCage Geocoder has a few optional parameters:

=over 1

=item Supported Parameters

please see L<the OpenCage geocoder documentation|>. Most of
L<the various optional parameters|> are supported. For example:

=over 2

=item language

An IETF format language code (such as es for Spanish or pt-BR for Brazilian
Portuguese); if this is omitted a code of en (English) will be assumed.

=item limit

Limits the maximum number of results returned. Default is 10.

=item countrycode

Provides the geocoder with a hint to the country that the query resides in.
This value will help the geocoder but will not restrict the possible results to
the supplied country.

The country code is a comma seperated list of 2 character code as defined by the ISO 3166-1 Alpha 2 standard.


=item Not Supported

=over 2

=item jsonp

This module always parses the response as a Perl data structure, so the jsonp
option is never used.



As a full example:

    my $result = $Geocoder->geocode(
        location => "Псковская улица, Санкт-Петербург, Россия",
        language => "ru",
        countrycode => "ru",

=head2 reverse_geocode

Takes two named parameters 'lat' and 'lng' and returns a result hashref.

    my $result = $Geocoder->reverse_geocode(lat => -22.6792, lng => 14.5272);

This method supports the optional parameters in the same way that geocode() does.


All strings passed to and received from Geo::Coder::OpenCage methods are
expected to be character strings, not byte strings.

For more information see L<perlunicode>.

=head1 SEE ALSO

This module was L<featured in the 2016 Perl Advent Calendar|>.

Ed Freyfogle from the OpenCage team gave L<an interview with Built in Perl about how Perl is used at OpenCage|>.

=head1 AUTHOR

Ed Freyfogle


Copyright 2019 OpenCage GmbH <>

Please check out all our open source work over at L<> and our developer blog: L<>


This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.16 or,
at your option, any later version of Perl 5 you may have available.

=head1 AUTHOR

edf <>


This software is copyright (c) 2020 by OpenCage GmbH.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.