/******************************************************************************
 *
 * Project:  OpenGIS Simple Features Reference Implementation
 * Purpose:  Client of geocoding service.
 * Author:   Even Rouault, <even dot rouault at spatialys.com>
 *
 ******************************************************************************
 * Copyright (c) 2012-2013, Even Rouault <even dot rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "cpl_port.h"
#include "ogr_geocoding.h"

#include <cstddef>
#include <cstring>
#include <string>

#include "cpl_conv.h"
#include "cpl_error.h"
#include "cpl_http.h"
#include "cpl_minixml.h"
#include "cpl_multiproc.h"
#include "cpl_string.h"
#include "ogr_core.h"
#include "ogr_feature.h"
#include "ogr_geometry.h"
#include "memdataset.h"
#include "ogrsf_frmts.h"

// Emulation of gettimeofday() for Windows.
#ifdef _WIN32

#include <time.h>
#include <windows.h>
#include <winsock.h>

// Recent mingw define struct timezone.
#if !(defined(__GNUC__) && defined(_TIMEZONE_DEFINED))
struct timezone
{
    int tz_minuteswest;  // Minutes W of Greenwich.
    int tz_dsttime;      // Type of DST correction.
};
#endif

constexpr int MICROSEC_IN_SEC = 1000000;

static int OGR_gettimeofday(struct timeval *tv,
                            struct timezone * /* tzIgnored */)
{
    FILETIME ft;
    GetSystemTimeAsFileTime(&ft);

    // In 100-nanosecond intervals since January 1, 1601 (UTC).
    GUIntBig nVal =
        (static_cast<GUIntBig>(ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
    nVal /= 10;  // To microseconds.
    // There are 11 644 473 600 seconds between 1601 and 1970.
    nVal -= static_cast<GUIntBig>(116444736) * 100 * MICROSEC_IN_SEC;
    tv->tv_sec = static_cast<long>(nVal / MICROSEC_IN_SEC);
    tv->tv_usec = static_cast<long>(nVal % MICROSEC_IN_SEC);

    return 0;
}

#define gettimeofday OGR_gettimeofday

#else  // !defined WIN32
#include <sys/time.h>
#endif  // WIN32

struct _OGRGeocodingSessionHS
{
    char *pszCacheFilename;
    char *pszGeocodingService;
    char *pszEmail;
    char *pszUserName;
    char *pszKey;
    char *pszApplication;
    char *pszLanguage;
    char *pszQueryTemplate;
    char *pszReverseQueryTemplate;
    bool bReadCache;
    bool bWriteCache;
    double dfDelayBetweenQueries;
    GDALDataset *poDS;
};

static CPLMutex *hOGRGeocodingMutex = nullptr;
static double dfLastQueryTimeStampOSMNominatim = 0.0;
static double dfLastQueryTimeStampMapQuestNominatim = 0.0;

static const char OSM_NOMINATIM_QUERY[] =
    "http://nominatim.openstreetmap.org/search?q=%s&format=xml&polygon_text=1";
static const char MAPQUEST_NOMINATIM_QUERY[] =
    "http://open.mapquestapi.com/nominatim/v1/search.php?q=%s&format=xml";
static const char YAHOO_QUERY[] = "http://where.yahooapis.com/geocode?q=%s";
static const char GEONAMES_QUERY[] =
    "http://api.geonames.org/search?q=%s&style=LONG";
static const char BING_QUERY[] =
    "http://dev.virtualearth.net/REST/v1/Locations?q=%s&o=xml";

static const char OSM_NOMINATIM_REVERSE_QUERY[] =
    "http://nominatim.openstreetmap.org/reverse?format=xml&lat={lat}&lon={lon}";
static const char MAPQUEST_NOMINATIM_REVERSE_QUERY[] =
    "http://open.mapquestapi.com/nominatim/v1/"
    "reverse.php?format=xml&lat={lat}&lon={lon}";
static const char YAHOO_REVERSE_QUERY[] =
    "http://where.yahooapis.com/geocode?q={lat},{lon}&gflags=R";
static const char GEONAMES_REVERSE_QUERY[] =
    "http://api.geonames.org/findNearby?lat={lat}&lng={lon}&style=LONG";
static const char BING_REVERSE_QUERY[] =
    "http://dev.virtualearth.net/REST/v1/Locations/"
    "{lat},{lon}?includeEntityTypes=countryRegion&o=xml";

static const char CACHE_LAYER_NAME[] = "ogr_geocode_cache";
static const char DEFAULT_CACHE_SQLITE[] = "ogr_geocode_cache.sqlite";
static const char DEFAULT_CACHE_CSV[] = "ogr_geocode_cache.csv";

static const char FIELD_URL[] = "url";
static const char FIELD_BLOB[] = "blob";

/************************************************************************/
/*                       OGRGeocodeGetParameter()                       */
/************************************************************************/

static const char *OGRGeocodeGetParameter(char **papszOptions,
                                          const char *pszKey,
                                          const char *pszDefaultValue)
{
    const char *pszRet = CSLFetchNameValue(papszOptions, pszKey);
    if (pszRet != nullptr)
        return pszRet;

    return CPLGetConfigOption(CPLSPrintf("OGR_GEOCODE_%s", pszKey),
                              pszDefaultValue);
}

/************************************************************************/
/*                      OGRGeocodeHasStringValidFormat()                */
/************************************************************************/

// Checks that pszQueryTemplate has one and only one occurrence of %s in it.
static bool OGRGeocodeHasStringValidFormat(const char *pszQueryTemplate)
{
    const char *pszIter = pszQueryTemplate;
    bool bValidFormat = true;
    bool bFoundPctS = false;
    while (*pszIter != '\0')
    {
        if (*pszIter == '%')
        {
            if (pszIter[1] == '%')
            {
                ++pszIter;
            }
            else if (pszIter[1] == 's')
            {
                if (bFoundPctS)
                {
                    bValidFormat = false;
                    break;
                }
                bFoundPctS = true;
            }
            else
            {
                bValidFormat = false;
                break;
            }
        }
        ++pszIter;
    }
    if (!bFoundPctS)
        bValidFormat = false;
    return bValidFormat;
}

/************************************************************************/
/*                       OGRGeocodeCreateSession()                      */
/************************************************************************/

/* clang-format off */
/**
 * \brief Creates a session handle for geocoding requests.
 *
 * Available papszOptions values:
 * <ul>
 * <li> "CACHE_FILE" : Defaults to "ogr_geocode_cache.sqlite" (or otherwise
 *                    "ogr_geocode_cache.csv" if the SQLite driver isn't
 *                    available). Might be any CSV, SQLite or PostgreSQL
 *                    datasource.
 * <li> "READ_CACHE" : "TRUE" (default) or "FALSE"
 * <li> "WRITE_CACHE" : "TRUE" (default) or "FALSE"
 * <li> "SERVICE": <a href="http://wiki.openstreetmap.org/wiki/Nominatim">"OSM_NOMINATIM"</a>
 *      (default), <a href="http://open.mapquestapi.com/nominatim/">"MAPQUEST_NOMINATIM"</a>,
 *      <a href="http://developer.yahoo.com/geo/placefinder/">"YAHOO"</a>,
 *      <a href="http://www.geonames.org/export/geonames-search.html">"GEONAMES"</a>,
 *      <a href="http://msdn.microsoft.com/en-us/library/ff701714.aspx">"BING"</a> or
 *       other value.
 *      Note: "YAHOO" is no longer available as a free service.
 * <li> "EMAIL": used by OSM_NOMINATIM. Optional, but recommended.
 * <li> "USERNAME": used by GEONAMES. Compulsory in that case.
 * <li> "KEY": used by BING. Compulsory in that case.
 * <li> "APPLICATION": used to set the User-Agent MIME header. Defaults
 *       to GDAL/OGR version string.
 * <li> "LANGUAGE": used to set the Accept-Language MIME header. Preferred
 *      language order for showing search results.
 * <li> "DELAY": minimum delay, in second, between 2 consecutive queries.
 *       Defaults to 1.0.
 * <li> "QUERY_TEMPLATE": URL template for GET requests. Must contain one
 *       and only one occurrence of %%s in it. If not specified, for
 *       SERVICE=OSM_NOMINATIM, MAPQUEST_NOMINATIM, YAHOO, GEONAMES or BING,
 *       the URL template is hard-coded.
 * <li> "REVERSE_QUERY_TEMPLATE": URL template for GET requests for reverse
 *       geocoding. Must contain one and only one occurrence of {lon} and {lat}
 *       in it.  If not specified, for SERVICE=OSM_NOMINATIM,
 *       MAPQUEST_NOMINATIM, YAHOO, GEONAMES or BING, the URL template is
 *       hard-coded.
 * </ul>
 *
 * All the above options can also be set by defining the configuration option
 * of the same name, prefixed by OGR_GEOCODE_. For example "OGR_GEOCODE_SERVICE"
 * for the "SERVICE" option.
 *
 * @param papszOptions NULL, or a NULL-terminated list of string options.
 *
 * @return a handle that should be freed with OGRGeocodeDestroySession(), or
 *         NULL in case of failure.
 *
 */
/* clang-format on */

OGRGeocodingSessionH OGRGeocodeCreateSession(char **papszOptions)
{
    OGRGeocodingSessionH hSession = static_cast<OGRGeocodingSessionH>(
        CPLCalloc(1, sizeof(_OGRGeocodingSessionHS)));

    const char *pszCacheFilename = OGRGeocodeGetParameter(
        papszOptions, "CACHE_FILE", DEFAULT_CACHE_SQLITE);
    CPLString osExt = CPLGetExtensionSafe(pszCacheFilename);
    if (!(STARTS_WITH_CI(pszCacheFilename, "PG:") || EQUAL(osExt, "csv") ||
          EQUAL(osExt, "sqlite")))
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Only .csv, .sqlite or PG: datasources are handled for now.");
        OGRGeocodeDestroySession(hSession);
        return nullptr;
    }
    hSession->pszCacheFilename = CPLStrdup(pszCacheFilename);

    hSession->bReadCache =
        CPLTestBool(OGRGeocodeGetParameter(papszOptions, "READ_CACHE", "TRUE"));
    hSession->bWriteCache = CPLTestBool(
        OGRGeocodeGetParameter(papszOptions, "WRITE_CACHE", "TRUE"));

    const char *pszGeocodingService =
        OGRGeocodeGetParameter(papszOptions, "SERVICE", "OSM_NOMINATIM");
    hSession->pszGeocodingService = CPLStrdup(pszGeocodingService);

    const char *pszEmail =
        OGRGeocodeGetParameter(papszOptions, "EMAIL", nullptr);
    hSession->pszEmail = pszEmail ? CPLStrdup(pszEmail) : nullptr;

    const char *pszUserName =
        OGRGeocodeGetParameter(papszOptions, "USERNAME", nullptr);
    hSession->pszUserName = pszUserName ? CPLStrdup(pszUserName) : nullptr;

    const char *pszKey = OGRGeocodeGetParameter(papszOptions, "KEY", nullptr);
    hSession->pszKey = pszKey ? CPLStrdup(pszKey) : nullptr;

    if (EQUAL(pszGeocodingService, "GEONAMES") && pszUserName == nullptr)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "GEONAMES service requires USERNAME to be specified.");
        OGRGeocodeDestroySession(hSession);
        return nullptr;
    }
    else if (EQUAL(pszGeocodingService, "BING") && pszKey == nullptr)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "BING service requires KEY to be specified.");
        OGRGeocodeDestroySession(hSession);
        return nullptr;
    }

    const char *pszApplication = OGRGeocodeGetParameter(
        papszOptions, "APPLICATION", GDALVersionInfo(""));
    hSession->pszApplication = CPLStrdup(pszApplication);

    const char *pszLanguage =
        OGRGeocodeGetParameter(papszOptions, "LANGUAGE", nullptr);
    hSession->pszLanguage = pszLanguage ? CPLStrdup(pszLanguage) : nullptr;

    const char *pszDelayBetweenQueries =
        OGRGeocodeGetParameter(papszOptions, "DELAY", "1.0");
    // coverity[tainted_data]
    hSession->dfDelayBetweenQueries = CPLAtofM(pszDelayBetweenQueries);

    const char *pszQueryTemplateDefault = nullptr;
    if (EQUAL(pszGeocodingService, "OSM_NOMINATIM"))
        pszQueryTemplateDefault = OSM_NOMINATIM_QUERY;
    else if (EQUAL(pszGeocodingService, "MAPQUEST_NOMINATIM"))
        pszQueryTemplateDefault = MAPQUEST_NOMINATIM_QUERY;
    else if (EQUAL(pszGeocodingService, "YAHOO"))
        pszQueryTemplateDefault = YAHOO_QUERY;
    else if (EQUAL(pszGeocodingService, "GEONAMES"))
        pszQueryTemplateDefault = GEONAMES_QUERY;
    else if (EQUAL(pszGeocodingService, "BING"))
        pszQueryTemplateDefault = BING_QUERY;
    const char *pszQueryTemplate = OGRGeocodeGetParameter(
        papszOptions, "QUERY_TEMPLATE", pszQueryTemplateDefault);

    if (pszQueryTemplate != nullptr &&
        !OGRGeocodeHasStringValidFormat(pszQueryTemplate))
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "QUERY_TEMPLATE value has an invalid format");
        OGRGeocodeDestroySession(hSession);
        return nullptr;
    }

    hSession->pszQueryTemplate =
        pszQueryTemplate ? CPLStrdup(pszQueryTemplate) : nullptr;

    const char *pszReverseQueryTemplateDefault = nullptr;
    if (EQUAL(pszGeocodingService, "OSM_NOMINATIM"))
        pszReverseQueryTemplateDefault = OSM_NOMINATIM_REVERSE_QUERY;
    else if (EQUAL(pszGeocodingService, "MAPQUEST_NOMINATIM"))
        pszReverseQueryTemplateDefault = MAPQUEST_NOMINATIM_REVERSE_QUERY;
    else if (EQUAL(pszGeocodingService, "YAHOO"))
        pszReverseQueryTemplateDefault = YAHOO_REVERSE_QUERY;
    else if (EQUAL(pszGeocodingService, "GEONAMES"))
        pszReverseQueryTemplateDefault = GEONAMES_REVERSE_QUERY;
    else if (EQUAL(pszGeocodingService, "BING"))
        pszReverseQueryTemplateDefault = BING_REVERSE_QUERY;
    const char *pszReverseQueryTemplate = OGRGeocodeGetParameter(
        papszOptions, "REVERSE_QUERY_TEMPLATE", pszReverseQueryTemplateDefault);

    if (pszReverseQueryTemplate != nullptr &&
        (strstr(pszReverseQueryTemplate, "{lat}") == nullptr ||
         strstr(pszReverseQueryTemplate, "{lon}") == nullptr))
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "REVERSE_QUERY_TEMPLATE value has an invalid format");
        OGRGeocodeDestroySession(hSession);
        return nullptr;
    }

    hSession->pszReverseQueryTemplate = (pszReverseQueryTemplate)
                                            ? CPLStrdup(pszReverseQueryTemplate)
                                            : nullptr;

    return hSession;
}

/************************************************************************/
/*                       OGRGeocodeDestroySession()                     */
/************************************************************************/

/**
 * \brief Destroys a session handle for geocoding requests.

 * @param hSession the handle to destroy.
 *
 */
void OGRGeocodeDestroySession(OGRGeocodingSessionH hSession)
{
    if (hSession == nullptr)
        return;
    CPLFree(hSession->pszCacheFilename);
    CPLFree(hSession->pszGeocodingService);
    CPLFree(hSession->pszEmail);
    CPLFree(hSession->pszUserName);
    CPLFree(hSession->pszKey);
    CPLFree(hSession->pszApplication);
    CPLFree(hSession->pszLanguage);
    CPLFree(hSession->pszQueryTemplate);
    CPLFree(hSession->pszReverseQueryTemplate);
    if (hSession->poDS)
        delete hSession->poDS;
    CPLFree(hSession);
}

/************************************************************************/
/*                        OGRGeocodeGetCacheLayer()                     */
/************************************************************************/

static OGRLayer *OGRGeocodeGetCacheLayer(OGRGeocodingSessionH hSession,
                                         bool bCreateIfNecessary,
                                         int *pnIdxBlob)
{
    GDALDataset *poDS = hSession->poDS;
    CPLString osExt = CPLGetExtensionSafe(hSession->pszCacheFilename);

    if (poDS == nullptr)
    {
        if (GDALGetDriverCount() == 0)
            GDALAllRegister();

        const bool bHadValue =
            CPLGetConfigOption("OGR_SQLITE_SYNCHRONOUS", nullptr) != nullptr;
        std::string oOldVal(CPLGetConfigOption("OGR_SQLITE_SYNCHRONOUS", ""));

        CPLSetThreadLocalConfigOption("OGR_SQLITE_SYNCHRONOUS", "OFF");

        poDS = GDALDataset::Open(hSession->pszCacheFilename,
                                 GDAL_OF_VECTOR | GDAL_OF_UPDATE, nullptr,
                                 nullptr, nullptr);
        if (poDS == nullptr &&
            EQUAL(hSession->pszCacheFilename, DEFAULT_CACHE_SQLITE))
        {
            poDS = GDALDataset::Open(DEFAULT_CACHE_CSV,
                                     GDAL_OF_VECTOR | GDAL_OF_UPDATE, nullptr,
                                     nullptr, nullptr);
            if (poDS != nullptr)
            {
                CPLFree(hSession->pszCacheFilename);
                hSession->pszCacheFilename = CPLStrdup(DEFAULT_CACHE_CSV);
                CPLDebug("OGR", "Switch geocode cache file to %s",
                         hSession->pszCacheFilename);
                osExt = "csv";
            }
        }

        if (bCreateIfNecessary && poDS == nullptr &&
            !STARTS_WITH_CI(hSession->pszCacheFilename, "PG:"))
        {
            auto poDriver = GetGDALDriverManager()->GetDriverByName(osExt);
            if (poDriver == nullptr &&
                EQUAL(hSession->pszCacheFilename, DEFAULT_CACHE_SQLITE))
            {
                CPLFree(hSession->pszCacheFilename);
                hSession->pszCacheFilename = CPLStrdup(DEFAULT_CACHE_CSV);
                CPLDebug("OGR", "Switch geocode cache file to %s",
                         hSession->pszCacheFilename);
                osExt = "csv";
                poDriver = GetGDALDriverManager()->GetDriverByName(osExt);
            }
            if (poDriver != nullptr)
            {
                char **papszOptions = nullptr;
                if (EQUAL(osExt, "SQLITE"))
                {
                    papszOptions =
                        CSLAddNameValue(papszOptions, "METADATA", "FALSE");
                }

                poDS = poDriver->Create(hSession->pszCacheFilename, 0, 0, 0,
                                        GDT_Unknown, papszOptions);

                if (poDS == nullptr &&
                    (EQUAL(osExt, "SQLITE") || EQUAL(osExt, "CSV")))
                {
                    CPLFree(hSession->pszCacheFilename);
                    hSession->pszCacheFilename =
                        CPLStrdup(VSIMemGenerateHiddenFilename(CPLSPrintf(
                            "%s.%s", CACHE_LAYER_NAME, osExt.c_str())));
                    CPLDebug("OGR", "Switch geocode cache file to %s",
                             hSession->pszCacheFilename);
                    poDS = poDriver->Create(hSession->pszCacheFilename, 0, 0, 0,
                                            GDT_Unknown, papszOptions);
                }

                CSLDestroy(papszOptions);
            }
        }

        CPLSetThreadLocalConfigOption("OGR_SQLITE_SYNCHRONOUS",
                                      bHadValue ? oOldVal.c_str() : nullptr);

        if (poDS == nullptr)
            return nullptr;

        hSession->poDS = poDS;
    }

    CPLPushErrorHandler(CPLQuietErrorHandler);
    OGRLayer *poLayer = poDS->GetLayerByName(CACHE_LAYER_NAME);
    CPLPopErrorHandler();

    if (bCreateIfNecessary && poLayer == nullptr)
    {
        char **papszOptions = nullptr;
        if (EQUAL(osExt, "SQLITE"))
        {
            papszOptions =
                CSLAddNameValue(papszOptions, "COMPRESS_COLUMNS", FIELD_BLOB);
        }
        poLayer =
            poDS->CreateLayer(CACHE_LAYER_NAME, nullptr, wkbNone, papszOptions);
        CSLDestroy(papszOptions);

        if (poLayer != nullptr)
        {
            OGRFieldDefn oFieldDefnURL(FIELD_URL, OFTString);
            OGRFieldDefn oFieldDefnBlob(FIELD_BLOB, OFTString);
            if (poLayer->CreateField(&oFieldDefnURL) != OGRERR_NONE ||
                poLayer->CreateField(&oFieldDefnBlob) != OGRERR_NONE)
            {
                return nullptr;
            }
            if (EQUAL(osExt, "SQLITE") ||
                STARTS_WITH_CI(hSession->pszCacheFilename, "PG:"))
            {
                const char *pszSQL = CPLSPrintf(
                    "CREATE INDEX idx_%s_%s ON %s(%s)", FIELD_URL,
                    poLayer->GetName(), poLayer->GetName(), FIELD_URL);
                poDS->ExecuteSQL(pszSQL, nullptr, nullptr);
            }
        }
    }

    int nIdxBlob = -1;
    if (poLayer == nullptr ||
        poLayer->GetLayerDefn()->GetFieldIndex(FIELD_URL) < 0 ||
        (nIdxBlob = poLayer->GetLayerDefn()->GetFieldIndex(FIELD_BLOB)) < 0)
    {
        return nullptr;
    }

    if (pnIdxBlob)
        *pnIdxBlob = nIdxBlob;

    return poLayer;
}

/************************************************************************/
/*                        OGRGeocodeGetFromCache()                      */
/************************************************************************/

static char *OGRGeocodeGetFromCache(OGRGeocodingSessionH hSession,
                                    const char *pszURL)
{
    CPLMutexHolderD(&hOGRGeocodingMutex);

    int nIdxBlob = -1;
    OGRLayer *poLayer = OGRGeocodeGetCacheLayer(hSession, FALSE, &nIdxBlob);
    if (poLayer == nullptr)
        return nullptr;

    char *pszSQLEscapedURL = CPLEscapeString(pszURL, -1, CPLES_SQL);
    poLayer->SetAttributeFilter(
        CPLSPrintf("%s='%s'", FIELD_URL, pszSQLEscapedURL));
    CPLFree(pszSQLEscapedURL);

    char *pszRet = nullptr;
    OGRFeature *poFeature = poLayer->GetNextFeature();
    if (poFeature != nullptr)
    {
        if (poFeature->IsFieldSetAndNotNull(nIdxBlob))
            pszRet = CPLStrdup(poFeature->GetFieldAsString(nIdxBlob));
        OGRFeature::DestroyFeature(poFeature);
    }

    return pszRet;
}

/************************************************************************/
/*                        OGRGeocodePutIntoCache()                      */
/************************************************************************/

static bool OGRGeocodePutIntoCache(OGRGeocodingSessionH hSession,
                                   const char *pszURL, const char *pszContent)
{
    CPLMutexHolderD(&hOGRGeocodingMutex);

    int nIdxBlob = -1;
    OGRLayer *poLayer = OGRGeocodeGetCacheLayer(hSession, TRUE, &nIdxBlob);
    if (poLayer == nullptr)
        return false;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());
    poFeature->SetField(FIELD_URL, pszURL);
    poFeature->SetField(FIELD_BLOB, pszContent);
    const bool bRet = poLayer->CreateFeature(poFeature) == OGRERR_NONE;
    delete poFeature;

    return bRet;
}

/************************************************************************/
/*                        OGRGeocodeMakeRawLayer()                      */
/************************************************************************/

static OGRLayerH OGRGeocodeMakeRawLayer(const char *pszContent)
{
    OGRMemLayer *poLayer = new OGRMemLayer("result", nullptr, wkbNone);
    const OGRFeatureDefn *poFDefn = poLayer->GetLayerDefn();
    OGRFieldDefn oFieldDefnRaw("raw", OFTString);
    poLayer->CreateField(&oFieldDefnRaw);
    OGRFeature *poFeature = new OGRFeature(poFDefn);
    poFeature->SetField("raw", pszContent);
    CPL_IGNORE_RET_VAL(poLayer->CreateFeature(poFeature));
    delete poFeature;
    return OGRLayer::ToHandle(poLayer);
}

/************************************************************************/
/*                  OGRGeocodeBuildLayerNominatim()                     */
/************************************************************************/

static OGRLayerH OGRGeocodeBuildLayerNominatim(CPLXMLNode *psSearchResults,
                                               const char * /* pszContent */,
                                               const bool bAddRawFeature)
{
    OGRMemLayer *poLayer = new OGRMemLayer("place", nullptr, wkbUnknown);
    const OGRFeatureDefn *poFDefn = poLayer->GetLayerDefn();

    CPLXMLNode *psPlace = psSearchResults->psChild;
    // First iteration to add fields.
    while (psPlace != nullptr)
    {
        if (psPlace->eType == CXT_Element &&
            (strcmp(psPlace->pszValue, "place") == 0 ||  // Nominatim.
             strcmp(psPlace->pszValue, "geoname") == 0))
        {
            CPLXMLNode *psChild = psPlace->psChild;
            while (psChild != nullptr)
            {
                const char *pszName = psChild->pszValue;
                if ((psChild->eType == CXT_Element ||
                     psChild->eType == CXT_Attribute) &&
                    poFDefn->GetFieldIndex(pszName) < 0 &&
                    strcmp(pszName, "geotext") != 0)
                {
                    OGRFieldDefn oFieldDefn(pszName, OFTString);
                    if (strcmp(pszName, "place_rank") == 0)
                    {
                        oFieldDefn.SetType(OFTInteger);
                    }
                    else if (strcmp(pszName, "lat") == 0)
                    {
                        oFieldDefn.SetType(OFTReal);
                    }
                    else if (strcmp(pszName, "lon") == 0 ||  // Nominatim.
                             strcmp(pszName, "lng") == 0)    // Geonames.
                    {
                        oFieldDefn.SetType(OFTReal);
                    }
                    poLayer->CreateField(&oFieldDefn);
                }
                psChild = psChild->psNext;
            }
        }
        psPlace = psPlace->psNext;
    }

    if (bAddRawFeature)
    {
        OGRFieldDefn oFieldDefnRaw("raw", OFTString);
        poLayer->CreateField(&oFieldDefnRaw);
    }

    psPlace = psSearchResults->psChild;
    while (psPlace != nullptr)
    {
        if (psPlace->eType == CXT_Element &&
            (strcmp(psPlace->pszValue, "place") == 0 ||   // Nominatim.
             strcmp(psPlace->pszValue, "geoname") == 0))  // Geonames.
        {
            bool bFoundLat = false;
            bool bFoundLon = false;
            double dfLat = 0.0;
            double dfLon = 0.0;

            // Iteration to fill the feature.
            OGRFeature *poFeature = new OGRFeature(poFDefn);

            for (CPLXMLNode *psChild = psPlace->psChild; psChild != nullptr;
                 psChild = psChild->psNext)
            {
                const char *pszName = psChild->pszValue;
                const char *pszVal = CPLGetXMLValue(psChild, nullptr, nullptr);
                if (!(psChild->eType == CXT_Element ||
                      psChild->eType == CXT_Attribute))
                {
                    // Do nothing.
                    continue;
                }
                const int nIdx = poFDefn->GetFieldIndex(pszName);
                if (nIdx >= 0)
                {
                    if (pszVal != nullptr)
                    {
                        poFeature->SetField(nIdx, pszVal);
                        if (strcmp(pszName, "lat") == 0)
                        {
                            bFoundLat = true;
                            dfLat = CPLAtofM(pszVal);
                        }
                        else if (strcmp(pszName, "lon") == 0 ||  // Nominatim.
                                 strcmp(pszName, "lng") == 0)    // Geonames.
                        {
                            bFoundLon = true;
                            dfLon = CPLAtofM(pszVal);
                        }
                    }
                }
                else if (strcmp(pszName, "geotext") == 0)
                {
                    if (pszVal != nullptr)
                    {
                        OGRGeometry *poGeometry = nullptr;
                        OGRGeometryFactory::createFromWkt(pszVal, nullptr,
                                                          &poGeometry);
                        if (poGeometry)
                            poFeature->SetGeometryDirectly(poGeometry);
                    }
                }
            }

            if (bAddRawFeature)
            {
                CPLXMLNode *psOldNext = psPlace->psNext;
                psPlace->psNext = nullptr;
                char *pszXML = CPLSerializeXMLTree(psPlace);
                psPlace->psNext = psOldNext;

                poFeature->SetField("raw", pszXML);
                CPLFree(pszXML);
            }

            // If we did not find an explicit geometry, build it from
            // the 'lon' and 'lat' attributes.
            if (poFeature->GetGeometryRef() == nullptr && bFoundLon &&
                bFoundLat)
                poFeature->SetGeometryDirectly(new OGRPoint(dfLon, dfLat));

            CPL_IGNORE_RET_VAL(poLayer->CreateFeature(poFeature));
            delete poFeature;
        }
        psPlace = psPlace->psNext;
    }
    return OGRLayer::ToHandle(poLayer);
}

/************************************************************************/
/*               OGRGeocodeReverseBuildLayerNominatim()                 */
/************************************************************************/

static OGRLayerH OGRGeocodeReverseBuildLayerNominatim(
    CPLXMLNode *psReverseGeocode, const char *pszContent, bool bAddRawFeature)
{
    CPLXMLNode *psResult = CPLGetXMLNode(psReverseGeocode, "result");
    CPLXMLNode *psAddressParts =
        CPLGetXMLNode(psReverseGeocode, "addressparts");
    if (psResult == nullptr || psAddressParts == nullptr)
    {
        return nullptr;
    }

    OGRMemLayer *poLayer = new OGRMemLayer("result", nullptr, wkbNone);
    const OGRFeatureDefn *poFDefn = poLayer->GetLayerDefn();

    bool bFoundLat = false;
    bool bFoundLon = false;
    double dfLat = 0.0;
    double dfLon = 0.0;

    // First iteration to add fields.
    CPLXMLNode *psChild = psResult->psChild;
    while (psChild != nullptr)
    {
        const char *pszName = psChild->pszValue;
        const char *pszVal = CPLGetXMLValue(psChild, nullptr, nullptr);
        if ((psChild->eType == CXT_Element ||
             psChild->eType == CXT_Attribute) &&
            poFDefn->GetFieldIndex(pszName) < 0)
        {
            OGRFieldDefn oFieldDefn(pszName, OFTString);
            if (strcmp(pszName, "lat") == 0)
            {
                if (pszVal != nullptr)
                {
                    bFoundLat = true;
                    dfLat = CPLAtofM(pszVal);
                }
                oFieldDefn.SetType(OFTReal);
            }
            else if (strcmp(pszName, "lon") == 0)
            {
                if (pszVal != nullptr)
                {
                    bFoundLon = true;
                    dfLon = CPLAtofM(pszVal);
                }
                oFieldDefn.SetType(OFTReal);
            }
            poLayer->CreateField(&oFieldDefn);
        }
        psChild = psChild->psNext;
    }

    {
        OGRFieldDefn oFieldDefn("display_name", OFTString);
        poLayer->CreateField(&oFieldDefn);
    }

    psChild = psAddressParts->psChild;
    while (psChild != nullptr)
    {
        const char *pszName = psChild->pszValue;
        if ((psChild->eType == CXT_Element ||
             psChild->eType == CXT_Attribute) &&
            poFDefn->GetFieldIndex(pszName) < 0)
        {
            OGRFieldDefn oFieldDefn(pszName, OFTString);
            poLayer->CreateField(&oFieldDefn);
        }
        psChild = psChild->psNext;
    }

    if (bAddRawFeature)
    {
        OGRFieldDefn oFieldDefnRaw("raw", OFTString);
        poLayer->CreateField(&oFieldDefnRaw);
    }

    // Second iteration to fill the feature.
    OGRFeature *poFeature = new OGRFeature(poFDefn);
    psChild = psResult->psChild;
    while (psChild != nullptr)
    {
        int nIdx = 0;
        const char *pszName = psChild->pszValue;
        const char *pszVal = CPLGetXMLValue(psChild, nullptr, nullptr);
        if ((psChild->eType == CXT_Element ||
             psChild->eType == CXT_Attribute) &&
            (nIdx = poFDefn->GetFieldIndex(pszName)) >= 0)
        {
            if (pszVal != nullptr)
                poFeature->SetField(nIdx, pszVal);
        }
        psChild = psChild->psNext;
    }

    const char *pszVal = CPLGetXMLValue(psResult, nullptr, nullptr);
    if (pszVal != nullptr)
        poFeature->SetField("display_name", pszVal);

    psChild = psAddressParts->psChild;
    while (psChild != nullptr)
    {
        int nIdx = 0;
        const char *pszName = psChild->pszValue;
        pszVal = CPLGetXMLValue(psChild, nullptr, nullptr);
        if ((psChild->eType == CXT_Element ||
             psChild->eType == CXT_Attribute) &&
            (nIdx = poFDefn->GetFieldIndex(pszName)) >= 0)
        {
            if (pszVal != nullptr)
                poFeature->SetField(nIdx, pszVal);
        }
        psChild = psChild->psNext;
    }

    if (bAddRawFeature)
    {
        poFeature->SetField("raw", pszContent);
    }

    // If we did not find an explicit geometry, build it from
    // the 'lon' and 'lat' attributes.
    if (poFeature->GetGeometryRef() == nullptr && bFoundLon && bFoundLat)
        poFeature->SetGeometryDirectly(new OGRPoint(dfLon, dfLat));

    CPL_IGNORE_RET_VAL(poLayer->CreateFeature(poFeature));
    delete poFeature;

    return OGRLayer::ToHandle(poLayer);
}

/************************************************************************/
/*                   OGRGeocodeBuildLayerYahoo()                        */
/************************************************************************/

static OGRLayerH OGRGeocodeBuildLayerYahoo(CPLXMLNode *psResultSet,
                                           const char * /* pszContent */,
                                           bool bAddRawFeature)
{
    OGRMemLayer *poLayer = new OGRMemLayer("place", nullptr, wkbPoint);
    const OGRFeatureDefn *poFDefn = poLayer->GetLayerDefn();

    // First iteration to add fields.
    CPLXMLNode *psPlace = psResultSet->psChild;
    while (psPlace != nullptr)
    {
        if (psPlace->eType == CXT_Element &&
            strcmp(psPlace->pszValue, "Result") == 0)
        {
            CPLXMLNode *psChild = psPlace->psChild;
            while (psChild != nullptr)
            {
                const char *pszName = psChild->pszValue;
                if ((psChild->eType == CXT_Element ||
                     psChild->eType == CXT_Attribute) &&
                    poFDefn->GetFieldIndex(pszName) < 0)
                {
                    OGRFieldDefn oFieldDefn(pszName, OFTString);
                    if (strcmp(pszName, "latitude") == 0)
                    {
                        oFieldDefn.SetType(OFTReal);
                    }
                    else if (strcmp(pszName, "longitude") == 0)
                    {
                        oFieldDefn.SetType(OFTReal);
                    }
                    poLayer->CreateField(&oFieldDefn);
                }
                psChild = psChild->psNext;
            }
        }

        psPlace = psPlace->psNext;
    }

    OGRFieldDefn oFieldDefnDisplayName("display_name", OFTString);
    poLayer->CreateField(&oFieldDefnDisplayName);

    if (bAddRawFeature)
    {
        OGRFieldDefn oFieldDefnRaw("raw", OFTString);
        poLayer->CreateField(&oFieldDefnRaw);
    }

    psPlace = psResultSet->psChild;
    while (psPlace != nullptr)
    {
        if (psPlace->eType == CXT_Element &&
            strcmp(psPlace->pszValue, "Result") == 0)
        {
            bool bFoundLat = false;
            bool bFoundLon = false;
            double dfLat = 0.0;
            double dfLon = 0.0;

            // Second iteration to fill the feature.
            OGRFeature *poFeature = new OGRFeature(poFDefn);
            for (CPLXMLNode *psChild = psPlace->psChild; psChild != nullptr;
                 psChild = psChild->psNext)
            {
                const char *pszName = psChild->pszValue;
                const char *pszVal = CPLGetXMLValue(psChild, nullptr, nullptr);
                if (!(psChild->eType == CXT_Element ||
                      psChild->eType == CXT_Attribute))
                {
                    // Do nothing.
                    continue;
                }
                const int nIdx = poFDefn->GetFieldIndex(pszName);
                if (nIdx >= 0)
                {
                    if (pszVal != nullptr)
                    {
                        poFeature->SetField(nIdx, pszVal);
                        if (strcmp(pszName, "latitude") == 0)
                        {
                            bFoundLat = true;
                            dfLat = CPLAtofM(pszVal);
                        }
                        else if (strcmp(pszName, "longitude") == 0)
                        {
                            bFoundLon = true;
                            dfLon = CPLAtofM(pszVal);
                        }
                    }
                }
            }

            CPLString osDisplayName;
            for (int i = 1;; ++i)
            {
                const int nIdx =
                    poFDefn->GetFieldIndex(CPLSPrintf("line%d", i));
                if (nIdx < 0)
                    break;
                if (poFeature->IsFieldSetAndNotNull(nIdx))
                {
                    if (!osDisplayName.empty())
                        osDisplayName += ", ";
                    osDisplayName += poFeature->GetFieldAsString(nIdx);
                }
            }
            poFeature->SetField("display_name", osDisplayName.c_str());

            if (bAddRawFeature)
            {
                CPLXMLNode *psOldNext = psPlace->psNext;
                psPlace->psNext = nullptr;
                char *pszXML = CPLSerializeXMLTree(psPlace);
                psPlace->psNext = psOldNext;

                poFeature->SetField("raw", pszXML);
                CPLFree(pszXML);
            }

            // Build geometry from the 'lon' and 'lat' attributes.
            if (bFoundLon && bFoundLat)
                poFeature->SetGeometryDirectly(new OGRPoint(dfLon, dfLat));

            CPL_IGNORE_RET_VAL(poLayer->CreateFeature(poFeature));
            delete poFeature;
        }
        psPlace = psPlace->psNext;
    }
    return OGRLayer::ToHandle(poLayer);
}

/************************************************************************/
/*                   OGRGeocodeBuildLayerBing()                         */
/************************************************************************/

static OGRLayerH OGRGeocodeBuildLayerBing(CPLXMLNode *psResponse,
                                          const char * /* pszContent */,
                                          bool bAddRawFeature)
{
    CPLXMLNode *psResources =
        CPLGetXMLNode(psResponse, "ResourceSets.ResourceSet.Resources");
    if (psResources == nullptr)
        return nullptr;

    OGRMemLayer *poLayer = new OGRMemLayer("place", nullptr, wkbPoint);
    const OGRFeatureDefn *poFDefn = poLayer->GetLayerDefn();

    // First iteration to add fields.
    CPLXMLNode *psPlace = psResources->psChild;
    while (psPlace != nullptr)
    {
        if (psPlace->eType == CXT_Element &&
            strcmp(psPlace->pszValue, "Location") == 0)
        {
            CPLXMLNode *psChild = psPlace->psChild;
            while (psChild != nullptr)
            {
                const char *pszName = psChild->pszValue;
                if ((psChild->eType == CXT_Element ||
                     psChild->eType == CXT_Attribute) &&
                    strcmp(pszName, "BoundingBox") != 0 &&
                    strcmp(pszName, "GeocodePoint") != 0 &&
                    poFDefn->GetFieldIndex(pszName) < 0)
                {
                    if (psChild->psChild != nullptr &&
                        psChild->psChild->eType == CXT_Element)
                    {
                        CPLXMLNode *psSubChild = psChild->psChild;
                        while (psSubChild != nullptr)
                        {
                            pszName = psSubChild->pszValue;
                            if ((psSubChild->eType == CXT_Element ||
                                 psSubChild->eType == CXT_Attribute) &&
                                poFDefn->GetFieldIndex(pszName) < 0)
                            {
                                OGRFieldDefn oFieldDefn(pszName, OFTString);
                                if (strcmp(pszName, "Latitude") == 0)
                                {
                                    oFieldDefn.SetType(OFTReal);
                                }
                                else if (strcmp(pszName, "Longitude") == 0)
                                {
                                    oFieldDefn.SetType(OFTReal);
                                }
                                poLayer->CreateField(&oFieldDefn);
                            }
                            psSubChild = psSubChild->psNext;
                        }
                    }
                    else
                    {
                        OGRFieldDefn oFieldDefn(pszName, OFTString);
                        poLayer->CreateField(&oFieldDefn);
                    }
                }
                psChild = psChild->psNext;
            }
        }
        psPlace = psPlace->psNext;
    }

    if (bAddRawFeature)
    {
        OGRFieldDefn oFieldDefnRaw("raw", OFTString);
        poLayer->CreateField(&oFieldDefnRaw);
    }

    // Iteration to fill the feature.
    psPlace = psResources->psChild;
    while (psPlace != nullptr)
    {
        if (psPlace->eType == CXT_Element &&
            strcmp(psPlace->pszValue, "Location") == 0)
        {
            bool bFoundLat = false;
            bool bFoundLon = false;
            double dfLat = 0.0;
            double dfLon = 0.0;

            OGRFeature *poFeature = new OGRFeature(poFDefn);
            for (CPLXMLNode *psChild = psPlace->psChild; psChild != nullptr;
                 psChild = psChild->psNext)
            {
                const char *pszName = psChild->pszValue;
                const char *pszVal = CPLGetXMLValue(psChild, nullptr, nullptr);
                if (!(psChild->eType == CXT_Element ||
                      psChild->eType == CXT_Attribute))
                {
                    // Do nothing.
                    continue;
                }
                const int nIdx = poFDefn->GetFieldIndex(pszName);
                if (nIdx >= 0)
                {
                    if (pszVal != nullptr)
                        poFeature->SetField(nIdx, pszVal);
                }
                else if (strcmp(pszName, "BoundingBox") != 0 &&
                         strcmp(pszName, "GeocodePoint") != 0 &&
                         psChild->psChild != nullptr &&
                         psChild->psChild->eType == CXT_Element)
                {
                    for (CPLXMLNode *psSubChild = psChild->psChild;
                         psSubChild != nullptr; psSubChild = psSubChild->psNext)
                    {
                        pszName = psSubChild->pszValue;
                        pszVal = CPLGetXMLValue(psSubChild, nullptr, nullptr);
                        if ((psSubChild->eType == CXT_Element ||
                             psSubChild->eType == CXT_Attribute))
                        {
                            const int nIdx2 = poFDefn->GetFieldIndex(pszName);
                            if (nIdx2 >= 0)
                            {
                                if (pszVal != nullptr)
                                {
                                    poFeature->SetField(nIdx2, pszVal);
                                    if (strcmp(pszName, "Latitude") == 0)
                                    {
                                        bFoundLat = true;
                                        dfLat = CPLAtofM(pszVal);
                                    }
                                    else if (strcmp(pszName, "Longitude") == 0)
                                    {
                                        bFoundLon = true;
                                        dfLon = CPLAtofM(pszVal);
                                    }
                                }
                            }
                        }
                    }
                }
            }

            if (bAddRawFeature)
            {
                CPLXMLNode *psOldNext = psPlace->psNext;
                psPlace->psNext = nullptr;
                char *pszXML = CPLSerializeXMLTree(psPlace);
                psPlace->psNext = psOldNext;

                poFeature->SetField("raw", pszXML);
                CPLFree(pszXML);
            }

            // Build geometry from the 'lon' and 'lat' attributes.
            if (bFoundLon && bFoundLat)
                poFeature->SetGeometryDirectly(new OGRPoint(dfLon, dfLat));

            CPL_IGNORE_RET_VAL(poLayer->CreateFeature(poFeature));
            delete poFeature;
        }
        psPlace = psPlace->psNext;
    }

    return OGRLayer::ToHandle(poLayer);
}

/************************************************************************/
/*                         OGRGeocodeBuildLayer()                       */
/************************************************************************/

static OGRLayerH OGRGeocodeBuildLayer(const char *pszContent,
                                      bool bAddRawFeature)
{
    OGRLayerH hLayer = nullptr;
    CPLXMLNode *psRoot = CPLParseXMLString(pszContent);
    if (psRoot != nullptr)
    {
        CPLXMLNode *psSearchResults = nullptr;
        CPLXMLNode *psReverseGeocode = nullptr;
        CPLXMLNode *psGeonames = nullptr;
        CPLXMLNode *psResultSet = nullptr;
        CPLXMLNode *psResponse = nullptr;
        if ((psSearchResults = CPLSearchXMLNode(psRoot, "=searchresults")) !=
            nullptr)
            hLayer = OGRGeocodeBuildLayerNominatim(psSearchResults, pszContent,
                                                   bAddRawFeature);
        else if ((psReverseGeocode =
                      CPLSearchXMLNode(psRoot, "=reversegeocode")) != nullptr)
            hLayer = OGRGeocodeReverseBuildLayerNominatim(
                psReverseGeocode, pszContent, bAddRawFeature);
        else if ((psGeonames = CPLSearchXMLNode(psRoot, "=geonames")) !=
                 nullptr)
            hLayer = OGRGeocodeBuildLayerNominatim(psGeonames, pszContent,
                                                   bAddRawFeature);
        else if ((psResultSet = CPLSearchXMLNode(psRoot, "=ResultSet")) !=
                 nullptr)
            hLayer = OGRGeocodeBuildLayerYahoo(psResultSet, pszContent,
                                               bAddRawFeature);
        else if ((psResponse = CPLSearchXMLNode(psRoot, "=Response")) !=
                 nullptr)
            hLayer = OGRGeocodeBuildLayerBing(psResponse, pszContent,
                                              bAddRawFeature);
        CPLDestroyXMLNode(psRoot);
    }
    if (hLayer == nullptr && bAddRawFeature)
        hLayer = OGRGeocodeMakeRawLayer(pszContent);
    return hLayer;
}

/************************************************************************/
/*                         OGRGeocodeCommon()                           */
/************************************************************************/

static OGRLayerH OGRGeocodeCommon(OGRGeocodingSessionH hSession,
                                  const std::string &osURLIn,
                                  char **papszOptions)
{
    std::string osURL(osURLIn);

    // Only documented to work with OSM Nominatim.
    if (hSession->pszLanguage != nullptr)
    {
        osURL += "&accept-language=";
        osURL += hSession->pszLanguage;
    }

    const char *pszExtraQueryParameters =
        OGRGeocodeGetParameter(papszOptions, "EXTRA_QUERY_PARAMETERS", nullptr);
    if (pszExtraQueryParameters != nullptr)
    {
        osURL += "&";
        osURL += pszExtraQueryParameters;
    }

    CPLString osURLWithEmail = osURL;
    if (EQUAL(hSession->pszGeocodingService, "OSM_NOMINATIM") &&
        hSession->pszEmail != nullptr)
    {
        char *const pszEscapedEmail =
            CPLEscapeString(hSession->pszEmail, -1, CPLES_URL);
        osURLWithEmail = osURL + "&email=" + pszEscapedEmail;
        CPLFree(pszEscapedEmail);
    }
    else if (EQUAL(hSession->pszGeocodingService, "GEONAMES") &&
             hSession->pszUserName != nullptr)
    {
        char *const pszEscaped =
            CPLEscapeString(hSession->pszUserName, -1, CPLES_URL);
        osURLWithEmail = osURL + "&username=" + pszEscaped;
        CPLFree(pszEscaped);
    }
    else if (EQUAL(hSession->pszGeocodingService, "BING") &&
             hSession->pszKey != nullptr)
    {
        char *const pszEscaped =
            CPLEscapeString(hSession->pszKey, -1, CPLES_URL);
        osURLWithEmail = osURL + "&key=" + pszEscaped;
        CPLFree(pszEscaped);
    }

    const bool bAddRawFeature =
        CPLTestBool(OGRGeocodeGetParameter(papszOptions, "RAW_FEATURE", "NO"));

    OGRLayerH hLayer = nullptr;

    char *pszCachedResult = nullptr;
    if (hSession->bReadCache)
        pszCachedResult = OGRGeocodeGetFromCache(hSession, osURL.c_str());
    if (pszCachedResult == nullptr)
    {
        double *pdfLastQueryTime = nullptr;
        if (EQUAL(hSession->pszGeocodingService, "OSM_NOMINATIM"))
            pdfLastQueryTime = &dfLastQueryTimeStampOSMNominatim;
        else if (EQUAL(hSession->pszGeocodingService, "MAPQUEST_NOMINATIM"))
            pdfLastQueryTime = &dfLastQueryTimeStampMapQuestNominatim;

        CPLString osHeaders = "User-Agent: ";
        osHeaders += hSession->pszApplication;
        if (hSession->pszLanguage != nullptr)
        {
            osHeaders += "\r\nAccept-Language: ";
            osHeaders += hSession->pszLanguage;
        }
        char **papszHTTPOptions =
            CSLAddNameValue(nullptr, "HEADERS", osHeaders.c_str());

        CPLHTTPResult *psResult = nullptr;
        if (pdfLastQueryTime != nullptr)
        {
            CPLMutexHolderD(&hOGRGeocodingMutex);
            struct timeval tv;

            gettimeofday(&tv, nullptr);
            double dfCurrentTime = tv.tv_sec + tv.tv_usec / 1e6;
            if (dfCurrentTime <
                *pdfLastQueryTime + hSession->dfDelayBetweenQueries)
            {
                CPLSleep(*pdfLastQueryTime + hSession->dfDelayBetweenQueries -
                         dfCurrentTime);
            }

            psResult = CPLHTTPFetch(osURLWithEmail, papszHTTPOptions);

            gettimeofday(&tv, nullptr);
            *pdfLastQueryTime = tv.tv_sec + tv.tv_usec / 1e6;
        }
        else
        {
            psResult = CPLHTTPFetch(osURLWithEmail, papszHTTPOptions);
        }

        CSLDestroy(papszHTTPOptions);
        papszHTTPOptions = nullptr;

        if (psResult == nullptr)
        {
            CPLError(CE_Failure, CPLE_AppDefined, "Query '%s' failed",
                     osURLWithEmail.c_str());
        }
        else
        {
            const char *pszResult =
                reinterpret_cast<const char *>(psResult->pabyData);
            if (pszResult != nullptr)
            {
                if (hSession->bWriteCache)
                {
                    // coverity[tainted_data]
                    OGRGeocodePutIntoCache(hSession, osURL.c_str(), pszResult);
                }
                hLayer = OGRGeocodeBuildLayer(pszResult, bAddRawFeature);
            }
            CPLHTTPDestroyResult(psResult);
        }
    }
    else
    {
        hLayer = OGRGeocodeBuildLayer(pszCachedResult, bAddRawFeature);
        CPLFree(pszCachedResult);
    }

    return hLayer;
}

/************************************************************************/
/*                              OGRGeocode()                            */
/************************************************************************/

/* clang-format off */
/**
 * \brief Runs a geocoding request.
 *
 * If the result is not found in cache, a GET request will be sent to resolve
 * the query.
 *
 * Note: most online services have Term of Uses. You are kindly requested
 * to read and follow them. For the OpenStreetMap Nominatim service, this
 * implementation will make sure that no more than one request is sent by
 * second, but there might be other restrictions that you must follow by other
 * means.
 *
 * In case of success, the return of this function is a OGR layer that contain
 * zero, one or several features matching the query. Note that the geometry of
 * the features is not necessarily a point.  The returned layer must be freed
 * with OGRGeocodeFreeResult().
 *
 * Note: this function is also available as the SQL
 * <a href="ogr_sql_sqlite.html#ogr_sql_sqlite_ogr_geocode_function">ogr_geocode()</a>
 * function of the SQL SQLite dialect.
 *
 * The list of recognized options is :
 * <ul>
 * <li>ADDRESSDETAILS=0 or 1: Include a breakdown of the address into elements
 *     Defaults to 1. (Known to work with OSM and MapQuest Nominatim)
 * <li>COUNTRYCODES=code1,code2,...codeN: Limit search results to a specific
 *     country (or a list of countries). The codes must fellow ISO 3166-1, i.e.
 *     gb for United Kingdom, de for Germany, etc.. (Known to work with OSM and
 *     MapQuest Nominatim)
 * <li>LIMIT=number: the number of records to return. Unlimited if not
 *     specified.  (Known to work with OSM and MapQuest Nominatim)
 * <li>RAW_FEATURE=YES: to specify that a 'raw' field must be added to the
 *     returned feature with the raw XML content.
 * <li>EXTRA_QUERY_PARAMETERS=params: additional parameters for the GET
 *     request.
 * </ul>
 *
 * @param hSession the geocoding session handle.
 * @param pszQuery the string to geocode.
 * @param papszStructuredQuery unused for now. Must be NULL.
 * @param papszOptions a list of options or NULL.
 *
 * @return a OGR layer with the result(s), or NULL in case of error.
 *         The returned layer must be freed with OGRGeocodeFreeResult().
 *
 */
/* clang-format on */

OGRLayerH OGRGeocode(OGRGeocodingSessionH hSession, const char *pszQuery,
                     char **papszStructuredQuery, char **papszOptions)
{
    VALIDATE_POINTER1(hSession, "OGRGeocode", nullptr);
    if ((pszQuery == nullptr && papszStructuredQuery == nullptr) ||
        (pszQuery != nullptr && papszStructuredQuery != nullptr))
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "Only one of pszQuery or papszStructuredQuery must be set.");
        return nullptr;
    }

    if (papszStructuredQuery != nullptr)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "papszStructuredQuery not yet supported.");
        return nullptr;
    }

    if (hSession->pszQueryTemplate == nullptr)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "QUERY_TEMPLATE parameter not defined");
        return nullptr;
    }

    constexpr const char *PCT_S = "%s";
    const char *pszPctS = strstr(hSession->pszQueryTemplate, PCT_S);
    if (!pszPctS)
    {
        // should not happen given OGRGeocodeHasStringValidFormat()
        return nullptr;
    }

    char *pszEscapedQuery = CPLEscapeString(pszQuery, -1, CPLES_URL);

    std::string osURL;
    osURL.assign(hSession->pszQueryTemplate,
                 pszPctS - hSession->pszQueryTemplate);
    osURL += pszEscapedQuery;
    osURL += (pszPctS + strlen(PCT_S));
    CPLFree(pszEscapedQuery);

    if (EQUAL(hSession->pszGeocodingService, "OSM_NOMINATIM") ||
        EQUAL(hSession->pszGeocodingService, "MAPQUEST_NOMINATIM"))
    {
        const char *pszAddressDetails =
            OGRGeocodeGetParameter(papszOptions, "ADDRESSDETAILS", "1");
        osURL += "&addressdetails=";
        osURL += pszAddressDetails;

        const char *pszCountryCodes =
            OGRGeocodeGetParameter(papszOptions, "COUNTRYCODES", nullptr);
        if (pszCountryCodes != nullptr)
        {
            osURL += "&countrycodes=";
            osURL += pszCountryCodes;
        }

        const char *pszLimit =
            OGRGeocodeGetParameter(papszOptions, "LIMIT", nullptr);
        if (pszLimit != nullptr && *pszLimit != '\0')
        {
            osURL += "&limit=";
            osURL += pszLimit;
        }
    }

    // coverity[tainted_data]
    return OGRGeocodeCommon(hSession, osURL, papszOptions);
}

/************************************************************************/
/*                      OGRGeocodeReverseSubstitute()                   */
/************************************************************************/

static CPLString OGRGeocodeReverseSubstitute(CPLString osURL, double dfLon,
                                             double dfLat)
{
    size_t iPos = osURL.find("{lon}");
    if (iPos != std::string::npos)
    {
        const CPLString osEnd(osURL.substr(iPos + 5));
        osURL = osURL.substr(0, iPos);
        osURL += CPLSPrintf("%.8f", dfLon);
        osURL += osEnd;
    }

    iPos = osURL.find("{lat}");
    if (iPos != std::string::npos)
    {
        const CPLString osEnd(osURL.substr(iPos + 5));
        osURL = osURL.substr(0, iPos);
        osURL += CPLSPrintf("%.8f", dfLat);
        osURL += osEnd;
    }

    return osURL;
}

/************************************************************************/
/*                         OGRGeocodeReverse()                          */
/************************************************************************/

/* clang-format off */
/**
 * \brief Runs a reverse geocoding request.
 *
 * If the result is not found in cache, a GET request will be sent to resolve
 * the query.
 *
 * Note: most online services have Term of Uses. You are kindly requested
 * to read and follow them. For the OpenStreetMap Nominatim service, this
 * implementation will make sure that no more than one request is sent by
 * second, but there might be other restrictions that you must follow by other
 * means.
 *
 * In case of success, the return of this function is a OGR layer that contain
 * zero, one or several features matching the query. The returned layer must be
 * freed with OGRGeocodeFreeResult().
 *
 * Note: this function is also available as the SQL
 * <a href="ogr_sql_sqlite.html#ogr_sql_sqlite_ogr_geocode_function">ogr_geocode_reverse()</a>
 * function of the SQL SQLite dialect.
 *
 * The list of recognized options is :
 * <ul>
 * <li>ZOOM=a_level: to query a specific zoom level. Only understood by the OSM
 *     Nominatim service.
 * <li>RAW_FEATURE=YES: to specify that a 'raw' field must be added to the
 *     returned feature with the raw XML content.
 * <li>EXTRA_QUERY_PARAMETERS=params: additional parameters for the GET request
 *     for reverse geocoding.
 * </ul>
 *
 * @param hSession the geocoding session handle.
 * @param dfLon the longitude.
 * @param dfLat the latitude.
 * @param papszOptions a list of options or NULL.
 *
 * @return a OGR layer with the result(s), or NULL in case of error.
 *         The returned layer must be freed with OGRGeocodeFreeResult().
 *
 */
/* clang-format on */

OGRLayerH OGRGeocodeReverse(OGRGeocodingSessionH hSession, double dfLon,
                            double dfLat, char **papszOptions)
{
    VALIDATE_POINTER1(hSession, "OGRGeocodeReverse", nullptr);

    if (hSession->pszReverseQueryTemplate == nullptr)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "REVERSE_QUERY_TEMPLATE parameter not defined");
        return nullptr;
    }

    CPLString osURL = hSession->pszReverseQueryTemplate;
    osURL = OGRGeocodeReverseSubstitute(osURL, dfLon, dfLat);

    if (EQUAL(hSession->pszGeocodingService, "OSM_NOMINATIM"))
    {
        const char *pszZoomLevel =
            OGRGeocodeGetParameter(papszOptions, "ZOOM", nullptr);
        if (pszZoomLevel != nullptr)
        {
            osURL = osURL + "&zoom=" + pszZoomLevel;
        }
    }

    // coverity[tainted_data]
    return OGRGeocodeCommon(hSession, osURL, papszOptions);
}

/************************************************************************/
/*                        OGRGeocodeFreeResult()                        */
/************************************************************************/

/**
 * \brief Destroys the result of a geocoding request.
 *
 * @param hLayer the layer returned by OGRGeocode() or OGRGeocodeReverse()
 *               to destroy.
 *
 */
void OGRGeocodeFreeResult(OGRLayerH hLayer)
{
    delete OGRLayer::FromHandle(hLayer);
}
