If you're reading this, you’ve probably hit one (or all) of these pain points with Google’s Geocoding API:
Geocoder.ca has been running since 2005 as a no-nonsense, developer-friendly alternative focused on Canada + USA.
| Feature | Google Maps Geocoding | Geocoder.ca |
|---|---|---|
| Pricing | $5 per 1,000 requests | $1–$1.50 per 1,000 (volume discounts) |
| Free daily quota | Effectively none after limits | No daily limits, credits never expire |
| Caching / storage of results | Restricted (legal headaches) | Fully allowed – cache forever |
| Canadian coverage (rural, French, PO boxes) | Often fails or low confidence | Excellent – built on Canada Post + provincial data |
| Rate limiting | Strict QPS + daily caps | Very generous (10–50 req/sec typical) |
Ready to switch? It’s usually <30 minutes of work.
No credit card required for tiny test amounts – you can start with the free demo mode (watermarked, 1 req/sec).
All examples below use the exact same endpoint structure:
Forward geocoding:
https://geocoder.ca/?locate=POSTAL_OR_ADDRESS&auth=YOUR_API_KEY&json=1
Reverse geocoding:
https://geocoder.ca/?latt=LAT&longt=LONG&reverse=1&auth=YOUR_API_KEY&json=1
import requests
API_KEY = "your_api_key_here"
def geocode_address(locate):
url = "https://geocoder.ca/"
params = {
"locate": locate,
"auth": API_KEY,
"json": 1
}
r = requests.get(url, params=params)
data = r.json()
if 'latt' in data:
return float(data['latt']), float(data['longt'])
else:
print("Error:", data.get('error', 'Unknown'))
return None
# Example
lat, lng = geocode_address("30 Wellington St W, Toronto, ON")
print(lat, lng) # → 43.647413 -79.38315
import httpx
import asyncio
async def geocode_async(client, locate):
params = {"locate": locate, "auth": API_KEY, "json": 1}
r = await client.get("https://geocoder.ca/", params=params)
data = r.json()
return (float(data['latt']), float(data['longt'])) if 'latt' in data else None
async def main():
async with httpx.AsyncClient() as client:
tasks = [geocode_async(client, addr) for addr in ["V6B 3K3", "90210", "K1P 1J1"]]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
const API_KEY = "your_api_key_here";
async function geocode(locate) {
const params = new URLSearchParams({
locate: locate,
auth: API_KEY,
json: 1
});
const res = await fetch(`https://geocoder.ca/?${params}`);
const data = await res.json();
if (data.latt) {
return { lat: parseFloat(data.latt), lng: parseFloat(data.longt) };
} else {
console.error(data.error || data);
return null;
}
}
// Usage
geocode("1000 Rue Saint-Antoine O, Montréal").then(console.log);
<?php
$api_key = "your_api_key_here";
function geocode_ca($locate) {
$url = "https://geocoder.ca/?locate=" . urlencode($locate)
. "&auth=$api_key&json=1";
$json = file_get_contents($url);
$data = json_decode($json, true);
if (isset($data['latt'])) {
return ['lat' => (float)$data['latt'], 'lng' => (float)$data['longt']];
}
return null;
}
print_r(geocode_ca("515 Legget Drive, Kanata, ON"));
?>
require 'net/http'
require 'json'
API_KEY = "your_api_key_here"
def geocode(locate)
uri = URI("https://geocoder.ca/")
params = { locate: locate, auth: API_KEY, json: 1 }
uri.query = URI.encode_www_form(params)
res = Net::HTTP.get_response(uri)
data = JSON.parse(res.body)
return { lat: data['latt'].to_f, lng: data['longt'].to_f } if data['latt']
nil
end
puts geocode("1600 Pennsylvania Ave NW, Washington")
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
type Result struct {
Latt string `json:"latt"`
Longt string `json:"longt"`
Error string `json:"error,omitempty"`
}
func geocode(locate string) (*Result, error) {
u, _ := url.Parse("https://geocoder.ca/")
q := u.Query()
q.Set("locate", locate)
q.Set("auth", "your_api_key_here")
q.Set("json", "1")
u.RawQuery = q.Encode()
resp, err := http.Get(u.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
var r Result
json.NewDecoder(resp.Body).Decode(&r)
if r.Error != "" {
return nil, fmt.Errorf(r.Error)
}
return &r, nil
}
func main() {
r, _ := geocode("M5V 3L9") // Toronto Union Station
fmt.Println(r.Latt, r.Longt)
}
#!/bin/bash
API_KEY="your_api_key_here"
LOCATE="100 King St W, Toronto"
curl -G "https://geocoder.ca/" \
--data-urlencode "locate=$LOCATE" \
--data-urlencode "auth=$API_KEY" \
--data-urlencode "json=1" | jq .
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
public class GeocoderCaExample {
private static final String API_KEY = "your_api_key_here";
private static final HttpClient client = HttpClient.newHttpClient();
private static final Gson gson = new Gson();
public static class Coordinates {
public double lat;
public double lng;
public Coordinates(double lat, double lng) {
this.lat = lat;
this.lng = lng;
}
}
public static Coordinates geocode(String locate) throws Exception {
String encodedLocate = URLEncoder.encode(locate, StandardCharsets.UTF_8);
String url = "https://geocoder.ca/?locate=" + encodedLocate
+ "&auth=" + API_KEY + "&json=1";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
JsonObject json = gson.fromJson(response.body(), JsonObject.class);
if (json.has("latt") && json.has("longt")) {
double lat = json.get("latt").getAsDouble();
double lng = json.get("longt").getAsDouble();
return new Coordinates(lat, lng);
} else {
String error = json.has("error") ? json.get("error").getAsString() : "Unknown error";
throw new RuntimeException("Geocoding failed: " + error);
}
}
public static void main(String[] args) throws Exception {
Coordinates coords = geocode("30 Wellington St W, Toronto, ON");
System.out.println("Lat: " + coords.lat + ", Lng: " + coords.lng);
// → Lat: 43.647413, Lng: -79.38315
}
}
OkHttpClient client = new OkHttpClient();
public Coordinates geocodeOkHttp(String locate) throws Exception {
HttpUrl url = HttpUrl.parse("https://geocoder.ca/").newBuilder()
.addQueryParameter("locate", locate)
.addQueryParameter("auth", API_KEY)
.addQueryParameter("json", "1")
.build();
Request request = new Request.Builder().url(url).build();
try (Response response = client.newCall(request).execute()) {
String body = response.body().string();
JsonObject json = new Gson().fromJson(body, JsonObject.class);
if (json.has("latt")) {
return new Coordinates(
json.get("latt").getAsDouble(),
json.get("longt").getAsDouble()
);
}
throw new RuntimeException("Error: " + json.get("error").getAsString());
}
}
object GeocoderCa {
private const val API_KEY = "your_api_key_here"
private val client = OkHttpClient()
private val gson = Gson()
data class Result(val latt: Double = 0.0, val longt: Double = 0.0, val error: String? = null)
fun geocode(locate: String, callback: (Result) -> Unit) {
val url = HttpUrl.Builder()
.scheme("https")
.host("geocoder.ca")
.addQueryParameter("locate", locate)
.addQueryParameter("auth", API_KEY)
.addQueryParameter("json", "1")
.build()
val request = Request.Builder().url(url).build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) = callback(Result(error = e.message))
override fun onResponse(call: Call, response: Response) {
response.body?.string()?.let { json ->
val result = gson.fromJson(json, Result::class.java)
callback(result)
} ?: callback(Result(error = "Empty response"))
}
})
}
}
// Usage
GeocoderCa.geocode("30 Wellington St W, Toronto, ON") { result ->
if (result.error == null) {
Log.d("Geocoder.ca", "Lat: ${result.latt}, Lng: ${result.longt}")
// → Lat: 43.647413, Lng: -79.38315
} else {
Log.e("Geocoder.ca", "Error: ${result.error}")
}
}
using System.Net.Http.Json;
using System.Text.Json;
var client = new HttpClient();
var key = "your_api_key_here";
async Task<(double? lat, double? lng)> GeocodeAsync(string locate)
{
var uri = $"https://geocoder.ca/?locate={Uri.EscapeDataString(locate)}&auth={key}&json=1";
var response = await client.GetFromJsonAsync<JsonElement>(uri);
if (response.TryGetProperty("latt", out var latProp) &&
response.TryGetProperty("longt", out var lngProp))
{
return (latProp.GetDouble(), lngProp.GetDouble());
}
return (null, null);
}
// Usage
var (lat, lng) = await GeocodeAsync("30 Wellington St W, Toronto, ON");
Console.WriteLine($"{lat}, {lng}"); // 43.647413, -79.38315
Just add these parameters:
latt=45.5017&longt=-73.5673&reverse=1&all=1
The all=1 flag returns street address + city + province/state + postal code – perfect for enriching GPS logs.
object GeocoderCa {
private const val API_KEY = "your_api_key_here"
private val client = OkHttpClient()
private val gson = Gson()
data class ReverseResult(
val standard: StandardAddress?,
val error: String? = null
)
data class StandardAddress(
val stnumber: String? = null,
val staddress: String? = null,
val city: String? = null,
val prov: String? = null, // CA province or US state
val postcode: String? = null,
val latt: Double = 0.0,
val longt: Double = 0.0
)
fun reverseGeocode(lat: Double, lng: Double, callback: (ReverseResult) -> Unit) {
val url = HttpUrl.Builder()
.scheme("https")
.host("geocoder.ca")
.addQueryParameter("latt", lat.toString())
.addQueryParameter("longt", lng.toString())
.addQueryParameter("reverse", "1")
.addQueryParameter("all", "1") // remove if you only need basic fields
.addQueryParameter("auth", API_KEY)
.addQueryParameter("json", "1")
.build()
val request = Request.Builder().url(url).build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) =
callback(ReverseResult(null, e.message))
override fun onResponse(call: Call, response: Response) {
response.body?.string()?.let { json ->
val result = gson.fromJson(json, ReverseResult::class.java)
callback(result)
} ?: callback(ReverseResult(null, "Empty response"))
}
})
}
}
// Usage
GeocoderCa.reverseGeocode(45.5017, -73.5673) { result ->
if (result.error == null && result.standard != null) {
with(result.standard) {
Log.d("Reverse", "$stnumber $staddress, $city, $prov $postcode")
// Example output:
// 1000 Rue de la Gauchetière O, Montréal, QC H3B 4W5
}
} else {
Log.e("Reverse", "Error: ${result.error}")
}
}
import requests
def reverse_geocode(lat, lng):
params = {
"latt": lat,
"longt": lng,
"reverse": 1,
"all": 1,
"auth": "your_api_key_here",
"json": 1
}
r = requests.get("https://geocoder.ca/", params=params).json()
return r.get("standard", {})
print(reverse_geocode(43.6532, -79.3832))
# → {'staddress': 'Toronto City Hall', 'city': 'Toronto', 'prov': 'ON', 'postcode': 'M5G 1P5', ...}
async function reverseGeocode(lat, lng) {
const params = new URLSearchParams({
latt: lat,
longt: lng,
reverse: 1,
all: 1,
auth: API_KEY,
json: 1
});
const res = await fetch(`https://geocoder.ca/?${params}`);
const data = await res.json();
return data.standard || { error: data.error };
}
reverseGeocode(49.2827, -123.1207).then(console.log);
// → { stnumber: '555', staddress: 'West Hastings St', city: 'Vancouver', prov: 'BC', ... }
One of the biggest hidden gems: geocoder.ca offers a lightweight, zero-dependency JavaScript autocomplete built specifically for Canadian addresses. It handles postal codes, rural routes, French street names, and PO boxes far better than global providers — and it’s only ~5 KB.
<script src="geocoderAutocomplete.js"></script>
3. Initialize in JavaScript (replace with your real API key!):
var autocomplete = new autocomplete({
input: document.getElementById("your_input_id"), // your address input field
key: 'your_api_key_here' // required - your geocoder.ca API key
// Optional: customize suggestion template
// itemtemplate: '<a href="#" class="list-group-item list-group-item-action"><strong>{{geocodeaddr}}</strong><br><small>{{city}}, {{prov}} {{postcode}}</small></a>'
});
Upload a plain-text file (one address per line) at
https://geocoder.ca/?batch=1
or use the API endpoint programmatically – way cheaper than doing 10,000 individual calls.
That’s it. Most developers finish the migration in under an hour and immediately see their monthly geocoding bill drop by 70–90% for North American traffic.
Questions? Hit me up in the comments or email [email protected] – happy to help you migrate!
P.S. If you found this guide useful, share it with one teammate who’s still paying Google tax 😄