Geolocation

Opensolr Geolocation — find answers to your questions

API - Geolocate IP Addresses (geo_lookup)

Logs & Analytics API

Geolocate IP Addresses

Turns public IP addresses into country, region, city, coordinates and timezone — up to 50 addresses in a single call. Use it to put a country column next to the addresses in your index request log, to map where your search traffic comes from, or to enrich your own application logs without running a geo-IP database yourself.

Endpoint

POST https://opensolr.com/solr_manager/api/geo_lookup
This one lives on opensolr.com. It is part of the management API, not the AI API, so do not call it on api.opensolr.com — that host answers 404 with WRONG_API_HOST and tells you the correct URL. GET works exactly like POST; a JSON request body is accepted too, with the same field names.

Parameters

ParameterTypeStatusDescription
emailstringRequiredYour Opensolr registration email address
api_keystringRequiredYour Opensolr API key (master key, or a scoped key that allows this endpoint)
ipsarray or stringOptionalThe addresses to look up. A real JSON array in a JSON body, a JSON array as a string, or a plain string of addresses separated by commas, semicolons or whitespace — all three are accepted, so a pasted log column works as-is
ipstringOptionalA single address. Can be used on its own, or together with ips — it is appended to that list
One of ips or ip is required. A request naming no usable address answers {"status": false, "msg": "ERROR_NO_IP_SUPPLIED"} together with the current max_ips value, so a client can discover the batch size without reading this page.

How It Works

Every address you send is validated first, then resolved. Duplicates are collapsed, so sending the same address forty times costs one lookup. Results already known to Opensolr are served from cache: a resolved address is cached for a week, an address the locator has no data for is cached for an hour, and an upstream failure is cached for one minute only — a transient outage never masquerades as “this address has no geography”.

Addresses with no geography are refused before any lookup happens: loopback, RFC1918 private ranges, CGNAT, link-local, multicast, documentation and reserved ranges, and their IPv6 equivalents (unique-local, link-local, Teredo, NAT64, documentation). An IPv4-mapped IPv6 address such as ::ffff:8.8.8.8 is unwrapped to its IPv4 form and judged by the IPv4 rules. These come back as ERROR_NOT_A_PUBLIC_IP inside the results rather than failing your request.

Results are keyed by the address exactly as you sent it (reduced to the characters an address can contain), so you can look up what you asked for without re-normalising anything. The ip field inside each entry is the canonical form.

Absent facts are absent

On a resolved entry only found, ip and country_code are guaranteed. Every other field is omitted when the locator does not know it — never an empty string, never a zero standing in for “unknown”. Test with isset() and you never have to guess whether a value is real:

  • Coordinates are omitted, not faked. A missing, non-numeric or out-of-range pair is dropped; so is exactly 0,0, which is a real place in the Gulf of Guinea and would put a false cluster on your map; and so is the locator's continental-US centroid when no city was resolved, which would otherwise spike every map in the middle of Kansas. Country and timezone are still returned in that case.
  • timezone is always a real IANA identifier (validated against the timezone database) or absent. Never an offset, never an invented string.
Versioned contract

The response shape is Opensolr's, not the locator's, and it carries contract_version. The provider behind the endpoint can be replaced without breaking your integration, and the field names on this page do not change when it is.

Limits

LimitValueWhat happens
Addresses per call50Extras are dropped, the first 50 are resolved, and the response sets capped: true. submitted tells you how many usable addresses you actually sent
Candidates parsed per call1000Parsing stops there, so a pathological payload cannot become the attack
Length per address64 charactersAnything longer is truncated before validation
Rate limit cost1 per addressYour monthly API request counter is charged per address, not per request: a 50-address batch costs 50. The per-minute and per-hour limits count the request once
IPv6 today. IPv6 input is accepted and fully validated, but the locator behind this endpoint is IPv4-only, so an IPv6 address currently answers ERROR_GEO_NO_DATA. The contract will not change when IPv6 support lands — the same entries simply start resolving.

Response

The envelope:

KeyTypeDescription
statusbooleantrue when the request was understood. Per-address failures do not make this false
contract_versionintegerVersion of this response shape. Currently 1
countintegerNumber of entries in results
submittedintegerUsable addresses your request contained, before the cap was applied
max_ipsintegerThe server-side batch cap in force
cappedbooleantrue only when submitted exceeded max_ips and addresses were dropped
resultsobjectOne entry per address, keyed by the address as you sent it

A resolved entry:

KeyTypeAlways presentDescription
foundbooleanYestrue
ipstringYesThe canonical form of the address
country_codestringYesTwo-letter ISO 3166 country code, uppercase
country_namestringNoCountry name
regionstringNoRegion or state name; falls back to the region code when only that is known
citystringNoCity name
latitudefloatNoPresent only when the pair is plottable — see Absent facts are absent
longitudefloatNoAs above; the two always appear together or not at all
timezonestringNoIANA timezone identifier, for example America/Chicago

An unresolved entry carries found: false, the canonical ip when the address was at least valid, and a msg naming the reason.

Errors

Per-address problems are not request failures — they appear inside results, so one bad line in a log batch never voids the other forty-nine:

CodeWhereMeaning
ERROR_NOT_A_PUBLIC_IPentryNot a valid address, or an address with no geography (private, loopback, CGNAT, link-local, reserved, multicast, documentation)
ERROR_GEO_NO_DATAentryA valid public address the locator has no data for. Also the current answer for IPv6
ERROR_GEO_LOOKUP_UNAVAILABLEentryThe lookup itself failed upstream. Retry — this is cached for one minute only
ERROR_NO_IP_SUPPLIEDrequestNeither ips nor ip named a usable address. HTTP 200 with status: false
ERROR_AUTHENTICATION_FAILEDrequestHTTP 403. Email / API key mismatch
ERROR_SCOPED_KEY_ENDPOINT_NOT_ALLOWEDrequestHTTP 403. A scoped key that was not granted this endpoint
ERROR_RATE_LIMIT_PER_MINUTE / _PER_HOURrequestHTTP 429 with a Retry-After header. Back off and retry
WRONG_API_HOSTrequestHTTP 404. You called it on api.opensolr.com; the body names the correct URL

Code Examples

$_ cURL

# A single address
curl -s "https://opensolr.com/solr_manager/api/geo_lookup?email=YOUR_EMAIL&api_key=YOUR_API_KEY&ip=8.8.8.8"

# A batch, as a plain separated list
curl -s -X POST "https://opensolr.com/solr_manager/api/geo_lookup" \
     -d "email=YOUR_EMAIL" -d "api_key=YOUR_API_KEY" \
     -d "ips=8.8.8.8, 1.1.1.1, 208.67.222.222"

# A batch, as a JSON body
curl -s -X POST "https://opensolr.com/solr_manager/api/geo_lookup" \
     -H "Content-Type: application/json" \
     -d '{"email":"YOUR_EMAIL","api_key":"YOUR_API_KEY","ips":["8.8.8.8","1.1.1.1"]}'

PHP PHP

$payload = json_encode([
    'email'   => 'YOUR_EMAIL',
    'api_key' => 'YOUR_API_KEY',
    'ips'     => ['8.8.8.8', '1.1.1.1', '10.0.0.5'],
]);

$ch = curl_init('https://opensolr.com/solr_manager/api/geo_lookup');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS     => $payload,
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);

if ($out['capped'] ?? false) {
    echo "Only the first {$out['max_ips']} of {$out['submitted']} addresses were resolved\n";
}

foreach ($out['results'] as $sent => $entry) {
    if (empty($entry['found'])) {
        echo "{$sent}: {$entry['msg']}\n";
        continue;
    }
    // Never assume city/coordinates are there — absent means unknown.
    $where = $entry['city'] ?? $entry['region'] ?? $entry['country_name'] ?? $entry['country_code'];
    $pin   = isset($entry['latitude']) ? " ({$entry['latitude']}, {$entry['longitude']})" : '';
    echo "{$sent}: {$entry['country_code']} — {$where}{$pin}\n";
}

Py Python

import requests

r = requests.post(
    "https://opensolr.com/solr_manager/api/geo_lookup",
    json={
        "email": "YOUR_EMAIL",
        "api_key": "YOUR_API_KEY",
        "ips": ["8.8.8.8", "1.1.1.1", "10.0.0.5"],
    },
    timeout=60,
)
data = r.json()

for sent, entry in data["results"].items():
    if not entry.get("found"):
        print(sent, entry["msg"])
        continue
    # .get() everywhere: an absent key means the locator did not know it.
    print(sent, entry["country_code"], entry.get("city"), entry.get("timezone"))

Example Response

{
  "status": true,
  "contract_version": 1,
  "count": 3,
  "submitted": 3,
  "max_ips": 50,
  "capped": false,
  "results": {
    "8.8.8.8": {
      "found": true,
      "ip": "8.8.8.8",
      "country_code": "US",
      "country_name": "United States",
      "region": "California",
      "city": "Mountain View",
      "latitude": 37.386,
      "longitude": -122.0838,
      "timezone": "America/Los_Angeles"
    },
    "1.1.1.1": {
      "found": true,
      "ip": "1.1.1.1",
      "country_code": "AU",
      "country_name": "Australia",
      "timezone": "Australia/Sydney"
    },
    "10.0.0.5": {
      "found": false,
      "msg": "ERROR_NOT_A_PUBLIC_IP"
    }
  }
}
Read the second entry again. 1.1.1.1 resolved to a country and a timezone and carries no city, region, latitude or longitude — because those were not known. That is the contract working as designed: your code branches on isset(), never on a sentinel value.

Use Cases

  • Add a country or city column to the addresses returned by the index request log and the traffic monitoring API, so you can see where your search traffic actually comes from
  • Draw a map of query volume by country without shipping a geo-IP database with your application, and without a per-lookup bill
  • Spot traffic that does not belong: a burst of queries from a country you do not serve usually means a leaked key, a scraper, or an integration nobody remembers deploying
  • Decide which region to host an index in, from where your users really are
  • Enrich your own application or web server logs — the endpoint does not care whether the addresses came from Opensolr
  • Set a per-user timezone from a signup address, using a value that is guaranteed to be a real IANA identifier

Related Documentation

Need help with the Opensolr API? We are here to help.

Contact Support
Read Full Answer

API - Nearest Places for GPS Coordinates (nearby_places)

Logs & Analytics API

Nearest Places for GPS Coordinates

Turns a pair of GPS coordinates into the nearest named place — city, region, province, community and country — and lists the other places within a radius. One pair or up to 50 pairs in a single call. It is what Opensolr Photos uses to write a photo's EXIF position into words, and it works the same for any application that has coordinates and wants a place name next to them.

Endpoint

POST https://opensolr.com/solr_manager/api/nearby_places
This one lives on opensolr.com. It is part of the management API, not the AI API, so do not call it on api.opensolr.com — that host answers 404 with WRONG_API_HOST and tells you the correct URL. GET works exactly like POST; a JSON request body is accepted too, with the same field names.

Parameters

ParameterTypeStatusDescription
emailstringRequiredYour Opensolr registration email address
api_keystringRequiredYour Opensolr API key (master key, or a scoped key that allows this endpoint)
coordsstring or arrayOptionalOne pair as lat,lon in decimal degrees, or several pairs separated by ; (a new line or | works too), or a JSON array of pairs. Several pairs switch the answer to the batch envelope below
lat, lonnumberOptionalOne pair as two separate fields, instead of coords
withinintegerOptionalRadius in kilometres to look for places in: 1 to 100, default 25
One of coords or lat + lon is required. A request naming no usable pair answers {"status": false, "msg": "ERROR_INVALID_COORDS"} together with max_coords. Latitude must be within ±90 and longitude within ±180; exactly 0,0 is refused as well, because it is where broken EXIF data points, not where a photo was taken.

How It Works

Each pair is rounded to four decimals (about 10 metres) and answered with the named places within the radius, nearest first, from a world-wide postal-place database. The nearest one is also returned on its own as nearest, which is the field an application usually wants.

Answers are kept. Once a pair has been answered with a place, every later request for the same pair and radius — from any account — is answered without touching the place database. “Nothing within the radius” and upstream failures are never kept: they are asked again next time, so a moment’s trouble or a country added later never stays a “no place here”.

In a batch, every pair is looked up on its own: the ones already answered come back at once, the unknown ones go to the place database together in one round trip, and each is answered under its own key, so one pair in the ocean never voids the other forty-nine.

Absent facts are absent

On a place entry only place, city, country_code and distance_km are guaranteed. region, province, community and country are omitted when the database does not know them for that place — never an empty string. Test with isset().

place is the name as the database has it, which for a large city is a postal-office name such as Paris 04; city is the same name with the trailing office number removed, which is the field to show and to index.

Versioned contract

The response shape is Opensolr's, not the database's, and it carries contract_version. The provider behind the endpoint can be replaced without breaking your integration.

Limits

LimitValueWhat happens
Pairs per call50Extras are dropped; the first 50 are answered
Radius1 to 100 kmAnything outside is clamped, default 25
Places per pair20Nearest first
Rate limit cost1 per requestCounted once against the per-minute, per-hour and monthly API request limits, whatever the number of pairs. This endpoint does not draw on the AI allowance

Response

A single pair answers the place envelope directly:

KeyTypeDescription
statusbooleantrue when at least one place was found within the radius
contract_versionintegerVersion of this response shape. Currently 1
coordsstringThe pair as answered, rounded to four decimals
within_kmintegerThe radius in force
countintegerNumber of entries in places
nearestobjectThe first entry of places
placesarrayUp to 20 place entries, nearest first

A place entry:

KeyTypeAlways presentDescription
placestringYesThe place name as the database has it
citystringYesThe place name without a trailing postal-office number
country_codestringYesTwo-letter ISO 3166 country code, uppercase
distance_kmfloatYesDistance from the pair, one decimal
regionstringNoRegion, state or county
provincestringNoProvince, where the country has them
communitystringNoCommunity, district or sector
countrystringNoCountry name
The street address on the spot

When a street address lies within 100 m of the pair, the envelope also carries address, the nearest one: street and country_code always, and number, unit, postal_code, city, district, region, province, country and distance_km when known. Street addresses cover the countries imported so far; elsewhere address is simply absent.

"address": { "street": "Roggenweg", "number": "8b", "postal_code": "21493", "city": "Schwarzenbek", "region": "Schleswig-Holstein", "country": "Germany", "country_code": "DE", "distance_km": 0.003 }

Several pairs answer the batch envelope: status: true, contract_version, within_km, count, and results — an object with one place envelope (or one error entry) per pair, keyed by the pair rounded to four decimals.

Errors

CodeWhereMeaning
ERROR_GEO_NO_DATAentryNo named place within the radius (open sea, polar regions). Not kept: asked again next time
ERROR_GEO_LOOKUP_UNAVAILABLEentryThe place database did not answer. Retry; not kept
ERROR_INVALID_COORDSrequestNo usable pair in the request. HTTP 200 with status: false
ERROR_AUTHENTICATION_FAILEDrequestHTTP 403. Email / API key mismatch
ERROR_SCOPED_KEY_ENDPOINT_NOT_ALLOWEDrequestHTTP 403. A scoped key that was not granted this endpoint
ERROR_RATE_LIMIT_PER_MINUTE / _PER_HOURrequestHTTP 429 with a Retry-After header. Back off and retry
WRONG_API_HOSTrequestHTTP 404. You called it on api.opensolr.com; the body names the correct URL

Code Examples

$_ cURL

# One pair
curl -s "https://opensolr.com/solr_manager/api/nearby_places?email=YOUR_EMAIL&api_key=YOUR_API_KEY&coords=48.8566,2.3522"

# One pair, as two fields, with a 10 km radius
curl -s -X POST "https://opensolr.com/solr_manager/api/nearby_places" \
     -d "email=YOUR_EMAIL" -d "api_key=YOUR_API_KEY" \
     -d "lat=30.2672" -d "lon=-97.7431" -d "within=10"

# A batch: pairs separated by semicolons
curl -s -X POST "https://opensolr.com/solr_manager/api/nearby_places" \
     -d "email=YOUR_EMAIL" -d "api_key=YOUR_API_KEY" \
     -d "coords=48.8566,2.3522;30.2672,-97.7431;0.5,0.5"

PHP PHP

$payload = json_encode([
    'email'   => 'YOUR_EMAIL',
    'api_key' => 'YOUR_API_KEY',
    'coords'  => ['48.8566,2.3522', '30.2672,-97.7431'],
    'within'  => 25,
]);

$ch = curl_init('https://opensolr.com/solr_manager/api/nearby_places');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS     => $payload,
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);

foreach ($out['results'] as $pair => $entry) {
    if (empty($entry['status'])) {
        echo "{$pair}: {$entry['msg']}\n";
        continue;
    }
    $near = $entry['nearest'];
    // region and country may be absent: absent means unknown.
    $where = implode(', ', array_filter([$near['city'], $near['region'] ?? null, $near['country'] ?? null]));
    echo "{$pair}: {$where} ({$near['distance_km']} km)\n";
}

Py Python

import requests

r = requests.post(
    "https://opensolr.com/solr_manager/api/nearby_places",
    json={
        "email": "YOUR_EMAIL",
        "api_key": "YOUR_API_KEY",
        "coords": ["48.8566,2.3522", "30.2672,-97.7431"],
    },
    timeout=30,
)
for pair, entry in r.json()["results"].items():
    if not entry.get("status"):
        print(pair, entry["msg"])
        continue
    near = entry["nearest"]
    # .get() for the optional fields: an absent key means the database did not know it.
    print(pair, near["city"], near.get("region"), near.get("country"), near["distance_km"], "km")

Example Response

A single pair:

{
  "status": true,
  "contract_version": 2,
  "coords": "30.2672,-97.7431",
  "within_km": 25,
  "count": 7,
  "nearest": {
    "place": "Austin",
    "city": "Austin",
    "country_code": "US",
    "distance_km": 0,
    "region": "Texas",
    "province": "Travis",
    "country": "United States of America"
  },
  "places": [
    { "place": "Austin", "city": "Austin", "country_code": "US", "distance_km": 0, "region": "Texas", "province": "Travis", "country": "United States of America" },
    { "place": "Mc Neil", "city": "Mc Neil", "country_code": "US", "distance_km": 7.1, "region": "Texas", "province": "Travis", "country": "United States of America" }
  ]
}

A batch, with one pair in the Gulf of Guinea:

{
  "status": true,
  "contract_version": 2,
  "within_km": 25,
  "count": 2,
  "results": {
    "48.8566,2.3522": {
      "status": true,
      "contract_version": 2,
      "coords": "48.8566,2.3522",
      "within_km": 25,
      "count": 20,
      "nearest": { "place": "Paris 04", "city": "Paris", "country_code": "FR", "distance_km": 0.4, "region": "Île-de-France", "province": "Paris", "community": "Paris", "country": "France" },
      "places": [ "..." ]
    },
    "0.5000,0.5000": {
      "status": false,
      "msg": "ERROR_GEO_NO_DATA",
      "coords": "0.5000,0.5000",
      "within_km": 25
    }
  }
}

Use Cases

  • Write the place a photo, a check-in or a sensor reading was taken at into your index in words, so that Lisbon is a search, a filter and an autocomplete entry — exactly what Opensolr Photos does at indexing time
  • Show “Austin, Texas” instead of 30.2672,-97.7431 anywhere a coordinate pair would otherwise be printed
  • Group records by city, region or country without a geocoding subscription: the answers are permanent and shared, so a busy application pays for a position once, ever
  • Pair it with geo_lookup: that one turns an address into coordinates, this one turns coordinates into the nearest named place

Related Documentation

Need help with the Opensolr API? We are here to help.

Contact Support
Read Full Answer

API - Find Places by Name (place_search)

Geolocation API

Find Places by Name

Turns what somebody typed — spring, Tulsa, Austin, Texas, United States — into the places it could be, each with its region, its country and its coordinates. It is the search box behind a map: a person names a place instead of dragging the world to it. Opensolr Photos uses it to move a photo to the right place, and it works the same for any application with an address field, a map or a location filter.

Endpoint

GET https://opensolr.com/solr_manager/api/place_search
This one lives on opensolr.com. It is part of the management API, not the AI API, so do not call it on api.opensolr.com — that host answers 404 with WRONG_API_HOST and tells you the correct URL. POST works exactly like GET; a JSON request body is accepted too, with the same field names.

Parameters

ParameterTypeStatusDescription
emailstringRequiredYour Opensolr registration email address
api_keystringRequiredYour Opensolr API key (master key, or a scoped key that grants place_search)
qstringRequiredThe name, or the beginning of a name, of a place. Up to 80 characters
countrystringOptionalTwo-letter country code (US, CA), to search inside one country only
limitintegerOptionalHow many places to return, 1 to 25. Ten by default

How It Works

The text is matched against a world-wide gazetteer of populated places and administrative divisions — cities, towns, villages, counties, countries. Names match as they are typed: spring already finds Springfield, because the name fields are indexed as prefixes, and diacritics are optional in either direction: montreal finds Montréal. Every language a place is known by is searched too, so New Orleans and La Nouvelle-Orléans both reach the same city.

The biggest place comes first. Results are ranked by population on a logarithmic scale, so a city of a million outranks a hamlet of the same name without burying it: what a person means is almost always the larger place, and the smaller ones are still on the list.

Commas are separators, not letters. Austin, Texas, United States searches exactly like Austin Texas United States, so the label you show a person is itself a query you can send back.

What it does not do

This endpoint knows places, not addresses: no streets, no house numbers, no points of interest. A street address is a different question, and it is answered from the other side: give nearby_places the coordinates you already have and it names the place around them.

Absent facts are absent

On a place entry only place, country_code, lat, lon and kind are guaranteed. region, province, country, population, locality and postal_code are omitted when the gazetteer does not know them — never an empty string. Test with isset().

kind reduces the gazetteer's feature codes to the kinds a person picking a place thinks in: city for a place to stand in, region for an administrative division inside a country, country for a country, address for a street with its house number, found when no town carries the name typed (Prieteniei 22). It is what to use when deciding how far to zoom a map.

Versioned contract

The response shape is Opensolr's, not the gazetteer's, and it carries contract_version. The provider behind the endpoint can be replaced without breaking your integration.

Limits

LimitValueWhat happens
Query length80 charactersLonger text is cut to 80
Places per call1 to 25Anything outside is clamped, default 10
Answers held24 hoursA name answered once is served from Opensolr for a day; the gazetteer itself is rebuilt monthly
Rate limit cost1 per requestCounted once against the per-minute, per-hour and monthly API request limits. This endpoint does not draw on the AI allowance
Typing is not searching. Wait until the typing stops before you call — about a third of a second — and ask only from the second character. One request per pause instead of one per letter keeps a search box inside a small rate limit, and holding the answers on your own side keeps it there for good.

Response

FieldTypeDescription
statusbooleantrue when places were found
contract_versionintegerThe version of this response shape
qstringThe text as it was searched for
country_codestringThe country filter that was applied, or null
countintegerHow many entries are in places
placesarrayThe places, best match first
places[].placestringThe name of the place
places[].regionstringCounty or state, when known
places[].provincestringThe division below the region, when known
places[].countrystringCountry name, when known
places[].country_codestringISO-3166-1 alpha-2 country code
places[].lat / places[].lonfloatDecimal degrees, five decimals
places[].kindstringcity, region, country or address
places[].populationintegerInhabitants, when the gazetteer knows a figure
places[].localitystringThe town of a street address (kind=address)
places[].postal_codestringThe postal code of a street address (kind=address)

Errors

HTTPMessageMeaning
400ERROR_NO_QUERY_SUPPLIEDNo usable text was sent in q
404ERROR_GEO_NO_DATANo place is known by that name. An answer, not a failure — but do not keep it: the name may be findable tomorrow
503ERROR_GEO_LOOKUP_UNAVAILABLEThe place database could not be asked. Retry later; nothing from a non-200 answer is cacheable
403ERROR_AUTHENTICATION_FAILEDEmail and API key do not match an account

Code Examples

$_ cURL

curl -s "https://opensolr.com/solr_manager/api/place_search" \
  --data-urlencode "email=you@example.com" \
  --data-urlencode "api_key=YOUR_API_KEY" \
  --data-urlencode "q=spring" \
  --data-urlencode "limit=5"

PHP PHP

$params = array(
    'email'   => 'you@example.com',
    'api_key' => 'YOUR_API_KEY',
    'q'       => 'austin, texas, united states',
    'limit'   => 10,
);
$url = 'https://opensolr.com/solr_manager/api/place_search?' . http_build_query($params);
$answer = json_decode(file_get_contents($url), true);

foreach ($answer['places'] as $place) {
    $label = $place['place']
        . (isset($place['region']) ? ', ' . $place['region'] : '')
        . (isset($place['country']) ? ', ' . $place['country'] : '');
    echo $label . ' -> ' . $place['lat'] . ',' . $place['lon'] . PHP_EOL;
}

Py Python

import requests

answer = requests.get(
    "https://opensolr.com/solr_manager/api/place_search",
    params={
        "email": "you@example.com",
        "api_key": "YOUR_API_KEY",
        "q": "springfield illinois",
        "limit": 5,
    },
    timeout=10,
).json()

for place in answer.get("places", []):
    print(place["place"], place.get("region"), place["lat"], place["lon"], place["kind"])

Example Response

{
  "status": true,
  "contract_version": 1,
  "q": "spring",
  "country_code": null,
  "count": 3,
  "places": [
    {
      "place": "Colorado Springs",
      "country_code": "US",
      "lat": 38.83388,
      "lon": -104.82136,
      "kind": "city",
      "region": "Colorado",
      "province": "El Paso County",
      "country": "United States",
      "population": 456568
    },
    {
      "place": "Spring Valley",
      "country_code": "US",
      "lat": 36.10803,
      "lon": -115.245,
      "kind": "city",
      "region": "Nevada",
      "province": "Clark County",
      "country": "United States",
      "population": 178395
    },
    {
      "place": "Springfield",
      "country_code": "US",
      "lat": 37.21533,
      "lon": -93.29824,
      "kind": "city",
      "region": "Missouri",
      "province": "Greene County",
      "country": "United States",
      "population": 170188
    }
  ]
}

And when nothing is known by that name:

{
  "status": false,
  "msg": "ERROR_GEO_NO_DATA",
  "q": "zzzqqq"
}

Use Cases

  • Put a search box on a map so a person can name the town instead of dragging the world to it — tap a result, fly the map to lat,lon, zoom by kind
  • Autocomplete a location field in a form, with the region and the country shown next to the name so two towns that share a name can be told apart
  • Turn a place a person typed into coordinates you can store, index and search on — the same coordinates nearby_places reads back as words
  • Offer a country, region or city filter built from real names rather than a hand-kept list
  • Pair it with geo_lookup and nearby_places: an IP in, coordinates out; coordinates in, a name out; a name in, coordinates out — the three directions of the same question

Related Documentation

Need help with the Opensolr API? We are here to help.

Contact Support
Read Full Answer