What's the best way to do geocoding in Foundry?

Hey,
I’d like to convert text addresses into coordinates from within the pipeline, either via transforms or pipeline builder.

I’m thinking of treating it as an External Transform in Transforms or via an UDF in Pipeline Builder, that would make the external call.

I was wondering if there’s a smarter way to do it, given there are mentions of Mapbox integrations for geospatial workflows in the doc, but I couldn’t find anything for geocoding.

Thanks !

Julien

We would also love to see this as a first-class platform feature. Very common workflow.

AFAIK, there is currently no way of doing this in a first class way!

We did it a few times by now and here are some recommendations I have from my experience doing this with regular Transforms:

Use an incremental pipeline that caches geocoding results: In an incremental pipeline you can read the previous output and add new geocoding results to the output.
This way you won’t need to constantly geocode the same adresses multiple times if they reappear.

Don’t use UDF’s: From my experience UDF’s create a ton of overhead and take up multiple workers. It’s much easer to just take a limited amount of Rows from dataframes and iterate through them with regular python code.
If you need parallelisation you can use Threads instead.
If you need to geocode more rows at once you can just run the pipeline more frequently and it will fill up you cache over time.

If anyone is interested I can search through some previous code and share some snippets!

Very interested in seeing some past examples! I am working through this problem now. Echo the sentiment that this would be a first-class platform feature

I know this is not immediately helpful, and we (pipeline builder) have heard this is an important workflow. We are building out a first class solution coming soon (~months)!

Awesome, thank you for the feedback!

Hi @david, may I please request a status update on this feature? I am looking for address verification solutions, preferably in Pipeline Builder.

Hi all :waving_hand: — thanks for the great discussion here!

I wanted to share a small snippet that might help others get started with geocoding in Foundry, especially if you’re looking to first test things locally before turning them into a pipeline or multi-threaded transformation. This approach uses the Nominatim API (OpenStreetMap), and can be a handy base to build on.

Here’s a Python example I’ve used to geocode U.S.-based addresses from a CSV file:

import pandas as pd
import requests
import time
import os
from tqdm import tqdm

tqdm.pandas()

def geocode(address, city, state):
    search_address = f"{address}, {city}, {state}, USA".strip().strip(',')

    if not search_address or search_address == "USA":
        return [None, None]

    base_url = "https://nominatim.openstreetmap.org/search"
    params = {
        "q": search_address,
        "format": "json",
        "limit": 1,
        "addressdetails": 1,
        "countrycodes": "us"
    }

    headers = {
        "User-Agent": "OSMGeoCode",
        "email" : "your_email"  # Replace with your contact email
    }

    try:
        response = requests.get(base_url, params=params, headers=headers)
        if response.status_code == 200:
            data = response.json()
            if data:
                lat = float(data[0]["lat"])
                lon = float(data[0]["lon"])
                time.sleep(1)  # Respect Nominatim rate limit
                return [lat, lon]

        time.sleep(1)
        return [None, None]

    except Exception:
        time.sleep(1)
        return [None, None]

How to use:

def geo_code(address, city, state):
    return geocode(address, city, state)

INPUT_FOLDER = 'input'
OUTPUT_FOLDER = 'output'
df = pd.read_csv(os.path.join(INPUT_FOLDER, 'addresses.csv'))

features_df = df.progress_apply(lambda row: geo_code(row.iloc[0], row.iloc[1], row.iloc[2]), axis=1, result_type='expand')
features_df.columns = ['lat', 'long']

result_df = pd.concat([df, features_df], axis=1)
result_df.to_csv(os.path.join(OUTPUT_FOLDER, 'geocoded_addresses.csv'), index=False)

A few notes:

  • This is designed for simple use cases — and mostly for small/medium datasets.
  • It respects Nominatim’s usage policy, including rate limits (1 request/sec).
  • You can easily imagine adapting this into a Foundry Code Workbook or converting it into a multi-threaded Spark transformation depending on scale.

Next steps:

  • Porting this into a Foundry transformation (happy to collaborate if others are on a similar path).
  • Adding multi-threaded capability for use with larger datasets (while still respecting API limits).

Hope this helps as a starting point — would love to hear how others are solving this too!

While Nominatim works technically, please remember it’s a free service run by volunteers. Respecting their rate limits is essential (1 request / second + need to specify an identifiable user-agent / email so they can contact you if something goes wrong).

For higher throughput geocoding, consider these alternatives:

  • Mapbox (https://www.mapbox.com/geocoding)
  • Google maps (https://developers.google.com/maps/documentation/geocoding/overview)
  • Self-hosting Nominatim (possible in Foundry but challenging; only worthwhile for processing millions of addresses)
  • Other commercial geocoding services

There is no native geocoding transform in Foundry — the Mapbox integration covers **Mapbox Boundaries** (choropleth region data) and the **Find Locations** UI feature in the Map application, both of which are visualization-layer features rather than pipeline-level geocoding capabilities. Your instinct to use an external call is the right approach, and Foundry supports this in two well-documented ways.

Option 1: Python External Transform (Code Repositories)

This is the most flexible approach for batch geocoding. You write a Python transform that calls a geocoding API (Mapbox Geocoding, Google Maps, etc.) using the `@use_external_systems` decorator with a network egress policy.

The recommended modern pattern is **source-based external transforms**. The legacy @use_external_systems approach still works but is in legacy status.

A minimal example calling an external geocoding endpoint:


from transforms.api import transform, Input, Output
from transforms.external.systems import use_external_systems, EgressPolicy, ExportControl
import requests
from pyspark.sql.functions import udf

from pyspark.sql.types import StringType
@use_external_systems(
    export_control=ExportControl(markings=\['<marking ID>'\]),
    egress=EgressPolicy('<egress policy RID>'),
)
@transform(
    output=Output('/path/to/geocoded/dataset'),
    addresses=Input('/path/to/addresses/dataset'),
)
def compute(export_control, egress, output, addresses):
    @udf(returnType=StringType())
    def geocode(address):
        response = requests.get(
            'https://api.mapbox.com/geocoding/v5/mapbox.places/' + address + '.json',
            params={'access_token': '<your_token>'},
            timeout=10
        )
        coords = response.json()\['features'\]\[0\]\['geometry'\]\['coordinates'\]
        return f"{coords\[1\]},{coords\[0\]}"  # lat,lon
    df = addresses.dataframe().withColumn('coordinates', geocode('address_column'))
    output.write_dataframe(df)

Key prerequisites:

  • The repository must be in **SECURE** mode.
  • An Information Security Officer must enable **Allow access to external systems** in the repository settings.
  • A network egress policy covering the geocoding API endpoint must be created in Control Panel and imported into the repository.
  • If your addresses dataset contains sensitive markings, those markings must be explicitly allowed under **Configure use of Foundry inputs with external systems**.

Option 2: Python UDF in Pipeline Builder

If you prefer to stay in Pipeline Builder, you can author a **Python function** that calls the geocoding API and use it as a UDF transform node in your pipeline.

The function runs as a sidecar container alongside the pipeline and scales dynamically. To make external API calls from it, you publish a Python function with access to external systems, then configure the source in Data Connection under **Connection settings > Code import configuration** to allow it to be imported into pipelines.

Once published and configured, you import the UDF into your pipeline via **Reusables > User-defined functions**, then wire it up as a transform node like any other.

Which to choose

External Transform (Code Repos) Python UDF (Pipeline Builder)
Best for Scheduled batch geocoding, complex logic, incremental processing Staying within a visual Pipeline Builder workflow
External API support Yes, via `@use_external_systems` Yes, via Python function with external system access
Governance Egress policy + export controls Source must be configured as pipeline-importable
Maturity Fully supported (use source-based, not legacy) Supported; runs as sidecar container

Both are valid. If geocoding is one step in a larger Pipeline Builder graph, the Python UDF route keeps everything in one place. If it is a standalone scheduled sync or you need incremental processing, the external transform in Code Repositories gives you more control.