Search along a route with CloudMade’s Local Search API

Suppose you go on a casual bike ride with a couple of friends. Wouldn’t it be nice to know if there are any coffee shops located along the route to refuel on caffeine and sugar? Or lets say you are doing a longer ride and would want to pick up a spare tube along the way. It would be good to know if there is any bike shop conveniently located along the route. Meet CloudMade’s Local Search API, which I used to power this specific feature in the most recent version of BikeNav. This article is going to show you how you can include that in your next mobile-aware app.

Step 1: Sign up for a Developer Account and get an API key

CloudMade lets you sign up for a developer account for free. After completing that initial step, access your account and click on the “Get API Key” button. Fill the details into the form presented to you, selecting “Web” as the platform and the “Free” pricing plan. Since we will not use any of the other features the API provides, the free version has more than we need.

Step 2: Create a token from your API key

In order to be able to communicate with CloudMade’s servers, you need to create an access token from your API key. This is done by sending an HTTP POST request to a particular authentication server as noted in their documentation. If you are using PHP, the following code can help you out (using Wez Furlong’s do_post_request function).

$apiKey = 'my-api-key';
$userId = 'your-generated-user-id';

// http://wezfurlong.org/blog/2006/nov/http-post-from-php-without-curl/
function do_post_request($url, $data, $optional_headers = null)
{
  $params = array('http' => array(
              'method' => 'POST',
              'content' => $data
            ));
  if ($optional_headers !== null) {
    $params['http']['header'] = $optional_headers;
  }
  $ctx = stream_context_create($params);
  $fp = @fopen($url, 'rb', false, $ctx);
  if (!$fp) {
    throw new Exception("Problem with $url, $php_errormsg");
  }
  $response = @stream_get_contents($fp);
  if ($response === false) {
    throw new Exception("Problem reading data from $url, $php_errormsg");
  }
  return $response;
}
$token =  do_post_request('http://auth.cloudmade.com/token/' . $apiKey . '?userid=' . $userId, '');

echo "generated token: " . $token;

Step 3: Requesting POI data along a given route

Equipped with the token from Step 2, we can now try to retrieve POIs along the route. The following example would do a search along the route for a maximum of 20 points of interest of type “cafe” from my place to Cupertino, within a radius of 1 kilometer and returning the data in JSONP format.

http://geocoding.cloudmade.com/[APIKEY]/geocoding/v2/find.js?token=[TOKEN]&object_type=cafe&along=37.36721,-122.03533,37.36756,-122.0354,37.36752,-122.03662,37.36672,-122.03607,37.36642,-122.03648,37.36429,-122.03288,37.33911,-122.03241,37.33763,-122.03239,37.32283,-122.03241&distance=1000&results=20&callback=mySuperCallback

The find method of the Local Search API lets you pass in various parameters, which are well documented. For the purpose of this article, the following are the most important ones:

  • object_type: the API lets you choose from an extensive list of point-of-interest types
  • along: vector of the route your are traveling along, i.e. a comma separated list of lat/long pairs
  • distance: radius of the search are in meters
  • results: number of results to return
  • callback: signaling the server to return the response in JSONP format and specifying a publicly accessible JavaScript function to execute, passing in the response data

The result from the above request will come back as JSONP and look similar to the following:

mySuperCallback({
    "found": "3",
    "bounds": [{
        "json": ["37.33155", "-122.03642"]
    }, {
        "json": ["37.36805", "-122.03266"]
    }],
    "features": [{
        "id": "7367785",
        "centroid": {
            "type": "POINT",
            "coordinates": ["37.36768", "-122.03642"]
        },
        "bounds": [{
            "json": ["37.36768", "-122.03642"]
        }, {
            "json": ["37.36768", "-122.03642"]
        }],
        "properties": {
            "operator": "Starbucks",
            "osm_element": "node",
            "amenity": "cafe",
            "synthesized_name": "Cafe",
            "osm_id": "266625630"
        },
        "type": "Feature"
    }, {
        "id": "26073213",
        "centroid": {
            "type": "POINT",
            "coordinates": ["37.33155", "-122.03266"]
        },
        "bounds": [{
            "json": ["37.33155", "-122.03266"]
        }, {
            "json": ["37.33155", "-122.03266"]
        }],
        "properties": {
            "osm_element": "node",
            "amenity": "cafe",
            "name": "Bagel Street Cafe",
            "osm_id": "957085286"
        },
        "type": "Feature"
    }, {
        "id": "14342531",
        "centroid": {
            "type": "POINT",
            "coordinates": ["37.36805", "-122.03338"]
        },
        "bounds": [{
            "json": ["37.36805", "-122.03338"]
        }, {
            "json": ["37.36805", "-122.03338"]
        }],
        "properties": {
            "osm_element": "node",
            "amenity": "cafe",
            "name": "Peet's Coffee",
            "osm_id": "441987522"
        },
        "type": "Feature"
    }],
    "type": "FeatureCollection",
    "crs": {
        "type": "EPSG",
        "properties": {
            "code": "4326",
            "coordinate_order": ["0", "1"]
        }
    }
});

In our mySuperCallback function we can then use this data to e.g. plot each of the returned points of interest on a map, in addition to the given route.

Be aware of long routes

Attentive readers will notice that there is a slight flaw in the way the request is constructed. If you have a long route with a lot of waypoints that you are passing along in the request to the search API, at some point you will reach the limit of the maximum amount of characters you can pass along in a URL. One way to mitigate this is to drop every other entry from the array of waypoints if the number exceeds a certain limit. That way, the search will still be somewhat accurate (since you are only taking out points in between instead of cutting them off at the beginning or the end). A JavaScript example code could look like this:

maxWaypoints = 160; // a good threshold that I noticed works well for BikeNav
waypoints = [ ... ]; // array where each entry represents a waypoint along the route
no_of_waypoints = waypoints.length;

if (no_of_waypoints > maxWaypoints) {
    while (waypoints.length > maxWaypoints) {
        for (var i = 0, len = waypoints.length; i < len; i++) {
            waypoints.splice(i + 2, 1);
        }
    }
}