Contents
2.3
Release date: 2026-09-28
Bug Fixes
- Fixed header callback reading past the provided buffer:
HeaderCallbackFunction()logged each header with"%s", reading until a NUL byte and ignoring the length libcurl provided. Now uses"%.*s"to print exactly the bytes handed over. - Fixed curl handle leaks on errors during request setup: Handle creation was not wrapped in
PG_TRY, so errors raised during parameter conversion or header construction bypassedcurl_easy_cleanup(). Handle is now tied to the current memory context with a reset callback, releasing it on any error path. - Fixed libxml2 string leaks on encoding conversion errors:
xml_get_prop()andxml_node_content()converted strings before freeing them, so encoding errors raised beforexmlFree()was called, leaking the libxml2-heap string. Both now convert and free on all paths, including error paths. - Fixed entrances losing their tags: The parser read
<entrance>attributes but ignored child<tag>elements. Tags are now collected into an “extratags” object per entrance. - Fixed control bytes in error messages: Error bodies containing NUL or control bytes were either cut at the first NUL or included raw in the log. Bodies are now sanitized byte-by-byte, with multi-byte UTF-8 sequences kept intact and non-printable bytes shown as
?. - Fixed acceptance of non-Nominatim responses: HTTP 200 responses were parsed without checking the root element, so a misconfigured endpoint returning JSON, empty body, or HTML went silently undetected. Parsers now validate root elements and report unexpected content.
- Fixed
polygon_thresholdlosing precision and accepting invalid values: Formatted with"%f"(six decimal places), it was rounded;NaN, infinity and negative values were forwarded as-is. Now formatted with"%.15g"and validated to reject non-finite and negative values. - Fixed
addressdetails=0not being sent for/reverseand/lookup: These endpoints default toaddressdetails=1, sonominatim_reverse(addressdetails => false)andnominatim_lookup(addressdetails => false)made the server compute the breakdown anyway. Both endpoints now explicitly requestaddressdetails=0. - Fixed request URLs with query strings or fragments: The URL was built as
<url> + "/" + <endpoint> + "?" + <params>. Aurlcontaining a query string got the endpoint appended to the query instead of the path, and fragments corrupted the request. URLs are now parsed and reassembled withcurl_url(), keeping existing query strings and appending the endpoint to the path. - Fixed validator accepting non-HTTP schemes for
url:file://,ftp://, … were accepted and failed only at first use. Validator now rejects any scheme other thanhttpandhttps. - Fixed retries on permanent transport errors:
IsRetryable()treated every libcurl error exceptCURLE_ABORTED_BY_CALLBACKas transient. Certificate verification failures, malformed URLs, unsupported protocols, etc. now fail immediately. Retries are reserved for network-level failures (DNS, connect, timeouts). - Fixed
nominatim_reverse()acceptingNaNcoordinates: Range checks likelat > 90.0are always false forNaN, so invalid coordinates passed validation. Now explicitly checkisnan(). - Fixed header injection via
accept_language: Control characters inaccept_languagewere pasted verbatim into HTTP headers, allowing CR/LF injection. NewCheckAcceptLanguage()rejects control characters in both the parameter and the server option. - Fixed encoding mismatches: Request parameters were percent-encoded in the database encoding instead of UTF-8; response values were stored as raw UTF-8 bytes, producing mojibake in non-UTF-8 databases. Parameters are now converted to UTF-8 before escaping, and response values are converted from UTF-8 to the database encoding.
- Fixed format string mismatches: Several
elog()calls passed arguments of mismatched types:"%ld"foruint64,"%ld"forsize_t,"%u"forOid. Now use appropriate format specifiers and casts. - Fixed
zoomout-of-range values being applied inconsistently: Values above 18 were sent as-is, values below -1 were dropped. Both are now clamped to 0 or 18 with aWARNINGnaming which value is used;-1(disabled) stays unchanged. - Fixed oversized responses failing inside libcurl:
WriteMemoryCallback()raised “invalid memory alloc request size” from within libcurl’s frames, making it impossible to distinguish from a real OOM. Responses now refuse data beyond a per-buffer limit by returning a short count, causing libcurl to abort cleanly withCURLE_WRITE_ERROR. Oversized responses are not retried.
Enhancements
- Added
max_response_sizeserver option: maximum size in bytes of a response body (default0, unlimited). A larger response is aborted as soon as it exceeds the limit, and the query fails with “response exceeds max_response_size limit of … bytes”, a hint to raise the limit, andERRCODE_PROGRAM_LIMIT_EXCEEDED. Independently of the option, a response can never exceed 1 GB, the most PostgreSQL can hold in a single buffer.
Breaking changes
- The query functions require the
USAGEprivilege on the foreign server: roles that used a server without it now getpermission denied for foreign server ...(see Security below). Grant it where it is needed:GRANT USAGE ON FOREIGN SERVER osm TO some_role;. - Invalid
polygon_thresholdvalues are rejected: negative,NaNand infinite values now raise an error instead of being forwarded to the server. zoomvalues below-1now mean country level: they are clamped to0, as the server does, instead of being dropped - which made the server fall back to building level (18).
Security
- Added USAGE privilege check on foreign servers: The query functions (
nominatim_search,nominatim_lookup,nominatim_reverse) looked up the server by name without verifying that the caller hasUSAGEon it. This bypassed the ACL checks thatCREATE FOREIGN TABLEenforces, allowing any role to send requests through any server — including with credentials of aPUBLICuser mapping.InitSession()now checksACL_USAGEand raises the standard “permission denied” error.
Improvements
- Made upgrade paths produce identical objects to fresh installs: Upgrades from 1.x and 2.2 left
NominatimRecord’s column order andnominatim_fdw_version()’s function properties different from a fresh install. Now consistent across all paths. - Fixed query cancellation sensitivity: The request and retry loop gave up on any
InterruptPendingflag, including non-cancellation interrupts likepg_log_backend_memory_contexts(), causing queries to fail when they should continue. Now checksQueryCancelPendingandProcDiePendingexplicitly. - Removed always-on verbose curl logging:
CURLOPT_VERBOSEwas on unconditionally, so every request was parsed forDEBUG3-level output and discarded. Now only enabled whenDEBUG3would actually be logged. - Removed reading of non-existent reverse parser fields:
ParseNominatimReverseData()readclass,typeandimportancefrom<result>, which Nominatim’s reverse endpoint never sends. Stopped reading them. - Improved debug output: DEBUG2 messages now log actual option values instead of the option names.
- Updated README: Clarified libxml2 minimum version (2.6.0, not 2.5.0), fixed example parameters, documented
zoomclamping, noted thaticonis alwaysNULLin reverse results. - Made regression tests opt-in for network access: Tests against a Nominatim server now require
INCLUDE_EXTERNAL_TESTS=1, preventing timeouts in isolated package-build environments. - Made builds reproducible: the build date reported by
nominatim_fdw_settings()honoursSOURCE_DATE_EPOCH, which package builds set, instead of always taking the wall clock. - Passed
longvalues to the libcurl options that expect them:CURLOPT_PROXYTYPEandCURLOPT_PROTOCOLSwere handedintconstants with libcurl releases before 8, whichcurl_easy_setopt()reads aslong.
2.2
Release date: 2026-09-10
Bug Fixes
- Fixed queries not being cancellable during an HTTP request:
CURLOPT_XFERINFOFUNCTIONwas registered without clearingCURLOPT_NOPROGRESS, which defaults to1and disables libcurl’s progress machinery entirely - the callback was therefore never invoked and theCHECK_FOR_INTERRUPTS()inside it never ran. Anominatim_search,nominatim_lookupornominatim_reversecall could not be interrupted withpg_cancel_backend()orCtrl+Cand blocked the backend until the server replied orconnect_timeout(default300seconds) expired.CURLOPT_NOPROGRESSis now explicitly set to0. - Fixed leak of the libcurl handle on interrupted requests: the progress callback raised the interrupt itself via
CHECK_FOR_INTERRUPTS(), soereport(ERROR)wouldlongjmpout of libcurl’s own call stack, skippingcurl_easy_cleanup()and leaking the easy handle and its socket for the lifetime of the backend. The callback now returns a non-zero value instead, which aborts the transfer withCURLE_ABORTED_BY_CALLBACK, and the pending interrupt is processed after the handle has been released. The request/retry loop is additionally wrapped inPG_TRY()/PG_CATCH()so that the handle is also released when an error is raised from a write callback (e.g. on out-of-memory). Aborted transfers and interrupts arriving during the inter-retry sleep no longer consume themax_connect_retryattempts. - Fixed missing libxml2 linkage: the Makefile passed
xml2-config --cflagsto the compiler but neverxml2-config --libsto the linker, sonominatim_fdw.sowas built with eleven unresolved libxml2 symbols and nolibxml2entry in its dynamic dependencies. This went unnoticed because the symbols happen to be provided by the backend itself whenever PostgreSQL was built with--with-libxml; against a PostgreSQL built without it,CREATE EXTENSIONfailed with an undefined-symbol error while loading the module.libxml2is now linked explicitly. - Fixed
max_connect_redirectof0allowing unlimited redirects: the redirect limit was only handed to libcurl when the configured value was non-zero, so0- the one value that unmistakably means “do not follow any redirect” - was the only value that leftCURLOPT_MAXREDIRSat its default of-1. A server redirecting in a loop was followed for 30 hops instead of none. The limit is now always applied, and0makes the request fail on the first redirect. - Fixed the server’s error message being discarded on failed requests:
CURLOPT_FAILONERRORmade libcurl throw away the response body of a4xx/5xxanswer, which is precisely where Nominatim explains what it objected to. A rejected request surfaced asThe requested URL returned error: 400and nothing else. HTTP status handling is now done by the wrapper, and the response body is reported as part of the error detail, truncated to 512 bytes so that a large HTML error page cannot flood the logs. - Fixed
<error>responses being silently reported as an empty result: Nominatim answers some requests with HTTP 200 and an<error>element - an un-geocodable coordinate, for instance, yieldsUnable to geocode. The parsers ignored that element, so the caller saw zero rows and no explanation. Such responses now raise aWARNINGcarrying the server’s message. - Fixed
Retry-Afterbeing ignored on rate-limited requests: response headers were collected on every request and then discarded without being read. A429 Too Many Requestsanswer is now retried after the delay the server asks for, capped at 30 seconds, instead of after a fixed one-second pause. Retries are also no longer attempted for client errors other than429, since an identical request would only be rejected again. - Fixed the inter-retry pause not reacting to query cancellation: the one-second
pg_usleep()between attempts neither processes nor notices interrupts, so a cancellation could sit unnoticed for up to one second per retry. The pause is now taken in 100 ms slices and abandoned as soon as an interrupt arrives. - Fixed
nominatim_fdw_handler()returning anFdwRoutinewith no callbacks: every planner callback was leftNULL, so a foreign table reaching the planner would have dereferenced a NULL function pointer. The handler now raises the same “FOREIGN TABLE not supported” error the validator does. No released version allowed such a table to be created, so this is a hardening fix. - Fixed use of
strtok()inIsLayerValid():strtok()keeps its parsing state in a process-wide static buffer, which is not safe in backend code. Replaced withstrtok_r(); the working copy of the string is also freed on the rejection path. - Fixed server options being parsed differently by the validator and at request time:
connect_timeout,max_connect_retryandmax_connect_redirectwere validated withstrtol()base0innominatim_fdw_validator()but read back with base10inInitSession(), so the two disagreed on any value that is not plain decimal. Aconnect_timeoutof'0x10'was accepted as 16 byCREATE SERVERand then silently used as0, and'010'was validated as 8 but used as 10. Both readings now go through a singleParseNonNegativeLong()helper, which also rejects values that overflowlong- previously accepted and clamped toLONG_MAX. Hexadecimal and octal notation are no longer accepted; values are always interpreted as decimal. - Fixed the endpoint URL being joined naively: a
urlwritten with a trailing slash - a natural way to write it - produced request URLs such ashttps://host//search?.... Trailing slashes are now trimmed before the request path is appended. - Fixed a negative
limit_resultbeing silently ignored:nominatim_search()dropped the parameter instead of complaining, so a caller passing a negative limit got the server default with no indication. Negative values are now rejected, consistent with how out-of-range coordinates are handled. - Fixed the libxml2 document leaking when parsing fails: the response document lives in libxml2’s heap rather than in a palloc context, so an error raised part-way through parsing - on a node that cannot be dumped, or on out-of-memory - abandoned it for the lifetime of the backend. Parsing is now wrapped so the document is released on the error path as well.
- Fixed libxml2 parse diagnostics going to stderr: an unparsable response body made libxml2 write directly to stderr, producing unstructured noise in the server log. The parser is now called with
XML_PARSE_NOERROR | XML_PARSE_NOWARNING; these are per-call options, so no global libxml2 error handler is installed and other users of the library in the same process are unaffected.
Breaking changes
extratags,namedetails,addressdetailsandentrancesare nowNULLwhen they were not requested: these columns were previously always populated, so a caller who leftextratagsat its default offalsestill got an empty{}back - indistinguishable from having asked for extra tags and the place having none. The empty object now carries that second meaning only, and “not requested” is reported asNULL. Queries that relied on these columns never beingNULL- ajsonboperator applied directly to the column, for instance, or aNOT NULLassumption - need to set the corresponding parameter totrue, or handleNULL.
Security
- Pinned
CURLOPT_UNRESTRICTED_AUTHto0, so that credentials from aUSER MAPPINGare never forwarded to a host the request was redirected to. This has always been libcurl’s default; setting it explicitly makes the intent visible and keeps it from changing underneath the wrapper.
Improvements
- Changed
nominatim_search(),nominatim_lookup()andnominatim_reverse()fromPARALLEL SAFEtoPARALLEL RESTRICTED: they perform HTTP requests, and the previous marking allowed PostgreSQL to run them inside parallel workers, so one query could issue several concurrent requests to the endpoint - something public Nominatim instances explicitly ask clients not to do. Plans that previously parallelised over these functions will now run serially. - Marked
nominatim_fdw_settings()asPARALLEL SAFE, matchingnominatim_fdw_version(). It only reports build information. - Removed the unused
custom_paramsandproxy_typefields from the internal session state.custom_paramswas never assigned at all, andproxy_typeonly ever held one value, making the test that guarded the proxy protocol always true. No behaviour changes. - Removed a redundant
text_to_cstring()call from each of the three query functions: theaccept_languageargument was converted twice per call. - Removed the unused
request_redirectfield from the internal session state. It was hardcoded totrueand never configurable, yet the code read as though redirects could be switched off independently ofmax_connect_redirect. Redirect behaviour is governed solely bymax_connect_redirect, where0means “do not follow any redirect”. No behaviour changes.
2.1
Release date: 2026-07-24
Enhancements
- Add HTTP basic authentication in
USER MAPPING: This feature defines a mapping of a PostgreSQL user to an user in the target Nominatim server -userandpassword, so that the user can be authenticated. - Add
request_timeoutserver option: sets the maximum time in seconds allowed for a complete HTTP request (CURLOPT_TIMEOUT), defaulting to0(no limit). The pre-existingconnect_timeoutonly bounds the connection phase, so a Nominatim server that accepted the connection and then stalled would occupy the backend indefinitely.
Bug fixes
- Fixed invalid libcurl lifecycle: Initialize libcurl’s global state once per backend via
_PG_init()(curl_global_init). Previously the wrapper relied on the implicit initialization performed bycurl_easy_init(), which libcurl documents as not thread-safe and unsafe when the address space is shared with other libcurl-using extensions (e.g.rdf_fdw).
2.0
Release date: 2026-07-07
Enhancements
- Add error message for invalid coordinate pairs: this adds a check on the reverse call to reject invalid coordinate pairs before sending the request to the server, therefore avoiding a HTTP request that is doomed to fail.
- Add
emailandpolygon_thresholdparameters to reverse function. - Add support to PostgreSQL 10 and 11 (EOL’d versions).
- Add system view
nominatim_fdw_settingsto list all library dependencies.
Bug fixes
- Fixed memory leaks in XML parsing:
xmlGetProp()andxmlNodeGetContent()return libxml2-heap-allocated strings that were never freed withxmlFree(). Introducedxml_get_prop()andxml_node_content()helper functions that copy the result into palloc’d memory and immediately free the libxml2 string, making ownership clear at a glance. - Fixed JSON injection in
extratags,namedetails,addressdetails, andaddresspartsfields: XML values from the Nominatim response were embedded into hand-crafted JSON strings without escaping, so values containing",\, or control characters produced malformedjsonbor allowed content injection from a malicious server. PostgreSQL’s ownescape_json()(fromutils/json.h) is now used to escape all keys and values before they are appended. - Fixed
nominatim_search,nominatim_lookup, andnominatim_reverseincorrectly declared asIMMUTABLE, which allowed PostgreSQL to cache or optimize away repeated calls and return stale results. Functions are now correctly declaredVOLATILE. - Fixed build failure when specifying a custom
PG_CONFIGpointing to a PostgreSQL installation built without--with-libxml. The Makefile now usesPG_CPPFLAGS(instead ofCFLAGS) and explicitly includesxml2-config --cflags, so libxml2 include paths are always passed to the compiler regardless of whichpg_configis used. - Add missing
typeattribute: the custom data typeNominatimRecordwas missing the attributetype. Thid has been now fixed. - Fix
DEFAULTvalue foraddressdetails: it now defaults totrue, as defined in the API spec. - Set
DEFAULTvalue of reverse’szoomto-1(disabled): the previous value was 0, which is a valid zoom level. - Fix parsing of
KMLgeometries iun reverse calls: the parser was ignoring this format and returningNULLforpolygon_kmlrequests. This is now fixed.
Breaking changes
- Add
entrancescolumn to lookup, search, and reverse calls. - Rename reverse’s column
resulttodisplay_name: the previous name was mimicing the xml node retrieved from the API, which was inconsistent with the lookup and search functions. - For simplicity,
nominatim_fdw_version()now omits ssl, zblib, libSSH, and ngt http2 versions. - Rename
addresspartscolumn from reverse function toaddressdetails, so that it aligns with search and lookup.
1.3
Release date: 2026-04-12
Breaking Changes
Proxy authentication credentials moved to USER MAPPING: For improved security, proxy authentication credentials (proxy_user and proxy_password) must now be specified in USER MAPPING instead of SERVER options. This change prevents proxy passwords from being visible to all users with USAGE privilege on the foreign server, as PostgreSQL automatically hides USER MAPPING passwords from non-owners.
1.2.0
Release date: 2026-04-05
Bug fixes
- Fixed
lon/latvalues of0.0being omitted from reverse geocoding requests. - Fixed memory leaks in
curl_easy_escapecalls. - Fixed undefined behaviour from
xmlFreeNodeon document-owned nodes; replaced withxmlFreeDoc. - Fixed
IsLayerValidrejecting valid comma-separated layer lists. - Fixed duplicate
state->amenityassignment innominatim_fdw_search. - Fixed redundant
palloc0forstateinnominatim_fdw_searchandnominatim_fdw_lookup. - Fixed early (incomplete) assignment of
place->addresspartsinParseNominatimReverseData.
Security
- Enabled TLS peer verification (
CURLOPT_SSL_VERIFYPEER).
Improvements
- Moved
curl_global_init/curl_global_cleanupto_PG_init/_PG_fini— called once per backend instead of once per request. - Made attribute name lookup in
GetAttributeValueconsistently useNameStr.
1.1.0
Release date: 2024-11-01
Enhancements
- Add support to PostgreSQL 17 and 18.