Skip to main content
Skip table of contents

How to compute metrics for set of points

This article will explain how to use Bulk point query - a new feature of Semantic layer.

The new functionality is especially well suited for usecase location planning. The other usecases can be:

  • The data analyst wants to evaluate his sites without using GIS or other precomputation in another tool. E.g. the number of ATMs/competitors/mobility in its neighborhood.

  • Data analyst wants to link his point data with each other or with point dimension.

  • Business Analyst wants to use a prebuilt template that has dimensions in it.

  • Business Analyst wants to filter by distance.

  • Data engineer, wants to use the Semantic Layer as a means to calculate the necessary values.

Source data

To use the Bulk point query, you need the following data prepared in your project:

Point datasets

The dataset referenced in each pointQuery must be a DWH dataset with geometryPoint subtype. The dataset must contain latitude and longitude properties that define the spatial location of each record. Enabling spatialIndex: true on the dataset is recommended for optimal query performance.

Examples of suitable datasets: customer locations, points of interest, postal code centroids, shop locations.

Input points

The input coordinates (the locations you want to evaluate) are not part of the project data โ€” they are passed directly in the API request body as an array of [longitude, latitude] pairs. These can represent, for example, candidate locations for a new store, delivery addresses, or any set of points you want to analyze against your existing datasets.

Typical metrics

The Bulk point query supports two fundamental query types โ€” isochrone and nearest โ€” that can be combined to build a comprehensive location profile.

For example, for each candidate location (Input point) you can compute turnover within walking distance, customer count within driving distance, the 2 nearest partner shops, and average distance to competition โ€” all in a single request. See the combined example below for a complete request and response.

Examples of typical metrics

  • Revenue / turnover within a walking or driving distance โ€” e.g. sum of transaction amounts within a 10-minute walk

  • Customer count within a driving distance โ€” e.g. number of customers reachable within a 15-minute drive

  • Point of interest (POI) count within a radius โ€” e.g. number of competitors, ATMs, or public transport stops within 200 meters

  • Metric breakdown by category โ€” any of the above grouped by a dimension, e.g. POI count broken down by type (accommodation, shopping, healthcare)

REST API

The Bulk point query is implemented as an asynchronous REST API endpoint. It means that the execution of a query has two phases:

  1. Submit a bulkPointQuery request as POST /rest/jobs endpoint. The accepted request returns a 202 HTTP status and the URL of created job (eg. /rest/jobs/db00de52-5ac8-0dc8-43d8-1cc162605007?type=bulkPointQuery).

  2. Polling on jobโ€™s URL. The GET request to /rest/jobs/{jobId} returns 200 and the status of task. The client should repeat the calls until the status of the jobs is changed from RUNNING to SUCCEDDED and the reponse body contains the result of Bulk point query.

REST API flow in curl

For a general information how to use CleverMaps API use REST API documentation

Submit a new request

CODE
curl --location --request POST 'https://secure.clevermaps.io/rest/jobs' \
--header 'Content-Type: application/json' \
--header 'Authorization: <BEARER_TOKEN>' \
--data-raw '{
  "type": "bulkPointQuery",
  "projectId": "kp3kgrr79b72vavc",
  "content": {
    "points": [
      {
        "id": "location_1",
        "lat": 52.47168,
        "lng": -1.8895
      }
    ],
    "pointQueries": [
      {
        "queryId": "transactions_foot_20_min",
        "type": "isochrone",
        "options": {
          "profile": "foot",
          "unit": "time",
          "amount": "20"
        },
        "dataset": "postcode_point_dwh",
        "properties": [
          {
            "id": "transaction_amount_sum",
            "type": "function_sum",
            "content": [
              {
                "type": "property",
                "value": "baskets.amount"
              }
            ]
          }
        ]
      },
      {
        "queryId": "transactions_foot_20_min_by_shop",
        "type": "isochrone",
        "options": {
          "profile": "foot",
          "unit": "time",
          "amount": "20"
        },
        "dataset": "postcode_point_dwh",
        "properties": [
          {
            "id": "shop_name",
            "type": "property",
            "value": "shops.name"
          },
          {
            "id": "transaction_amount_sum",
            "type": "function_sum",
            "content": [
              {
                "type": "property",
                "value": "baskets.amount"
              }
            ]
          }
        ],
        "filterBy": [
          {
            "property": "shops.partner",
            "value": "no",
            "operator": "eq"
          }
        ]        
      }
    ]
  }
}'

The bulkPointQuery above computes two metrics for each point defined in array points:

  1. Sum of transactions (property baskets.amount) in 20 minutes foot-walking area from each of points

  2. Sum of transactions in the same area but grouped by shops (property shops.name). The shops must match attribute filter shop.partner = 'no'.

Expected result status is 202 and the following JSON response:

JSON
{
  "id": "6bbed7c7-f0d7-4b61-86ac-f1a01adef894",
  "type": "bulkPointQuery",
  "status": "RUNNING",
  "startDate": 1723209621390,
  "links": [
    {
      "rel": "self",
      "href": "/rest/jobs/6bbed7c7-f0d7-4b61-86ac-f1a01adef894?type=bulkPointQuery"
    }
  ]
}

Get a result

For getting a result make a GET request on returned job url:

CODE
curl --location --request GET 'https://secure.clevermaps.io/rest/jobs/6bbed7c7-f0d7-4b61-86ac-f1a01adef894?type=bulkPointQuery' \
--header 'Authorization: <BEARER_TOKEN>'

Expected HTTP status is 200.

If the query is already executed, the status has value SUCCEEDED and the JSON response body contains a key result containing Bulk point query result:

CODE
{
    "id": "6bbed7c7-f0d7-4b61-86ac-f1a01adef894",
    "type": "bulkPointQuery",
    "status": "SUCCEEDED",
    "startDate": 1723209311668,
    "endDate": 1723209312541,
    "result": {
        "processedPoints": [
            {
                "id": "location_1",
                "lat": 52.47168,
                "lng": -1.8895,
                "content": [
                    {
                        "queryId": "transactions_foot_20_min",
                        "content": [
                            {
                                "transaction_amount_sum": 176544.56
                            }
                        ]
                    },
                    {
                        "queryId": "transactions_foot_20_min_by_shop",
                        "content": [
                            {
                                "shop_name": "My Store: Church Road - Bournbrook",
                                "transaction_amount_sum": 490.78
                            },
                            {
                                "shop_name": "My Store: Fort Shopping Centre",
                                "transaction_amount_sum": 343.5
                            },
                            {
                                "shop_name": "My Store: Grand Central",
                                "transaction_amount_sum": 128236.34
                            },
                            {
                                "shop_name": "My Store: Lower Precinct - Coventry",
                                "transaction_amount_sum": 750.15
                            }
                        ]
                    }
                ]
            }
        ],
        "consumedCredits": 0
    },
    "links": [
        {
            "rel": "self",
            "href": "/rest/jobs/6bbed7c7-f0d7-4b61-86ac-f1a01adef894?type=bulkPointQuery"
        }
    ]
}

If the status is RUNNING repeat the GET request after few seconds timeout and wait until the execution is finished.

The status FAILED, the response contains more details about error, e.g. the following error is returned when the foot time for the isochrone exceeded the maximal value 60 minutes:

CODE
{
    "id": "beb40489-e240-4f37-bb36-77b274e5e64a",
    "type": "bulkPointQuery",
    "status": "FAILED",
    "startDate": 1723210168010,
    "endDate": 1723210169561,
    "message": "Invalid time amount=2000. The maximum amount that can be specified is 60 minutes.",
    "links": [
        {
            "rel": "self",
            "href": "/rest/jobs/beb40489-e240-4f37-bb36-77b274e5e64a?type=bulkPointQuery"
        }
    ]
}

Bulk point query request

Key

Type

Optionality

Description

Constraints

type

string

REQUIRED

asynchronous job execution type

bulkPointQuery

projectId

string

REQUIRED

id of CleverMaps project for execution the Bulk point query

 

content

object

REQUIRED

Bulk point query request

 

content

Key

Type

Optionality

Description

Constraints

points

array

REQUIRED

list of coordinates objects.

pointQueries

array

REQUIRED

list of queries to be executed on each point from array points

 

content.points

An array of objects described bellow. The Bulk point queries are executed for each point defined in this array.

Key

Type

Optionality

Description

Constraints

id

string

OPTIONAL

Optional identifier of the point. The identifier is present in Bulk point query result and can simplify the parsing of response.

lat

number

REQUIRED

latitude coordinate of the point

lng

number

REQUIRED

longitude coordinate of the point

content.pointQueries

An array of objects described bellow.

Key

Type

Optionality

Description

Constraints

queryId

string

REQUIRED

an identifier of each query, it is used for grouping query results in the bulkPointQuery response

type

string

REQUIRED

a type of query. There are suported two types of queries:

  • isochrone

  • nearest

isochrone or nearest

options

object

REQUIRED

additional paremeters for query type. See more details bellow.

dataset

string

REQUIRED

name of geometryPoint dataset for executiond the query

properties

array

REQUIRED

list of query properties to be calculated by the query

content.type.isochrone.options

The isochrone is a line that connects points of equal travel time around a given location and can be calculated as:

  • an area reachable within a specified amount of time (see car, bike and foot profiles)

  • or a direct distance (circle) defined by meters from a point (see air profile).

The definition of isochrone parameters is in options object:

Key

Type

Optionality

Description

Constraints

profile

string

REQUIRED

The type of isochrone profile

car, bike, foot, air

unit

string

REQUIRED

Unit of computed isochrone

time or distance

amount

number

REQUIRED

Number of minutes or meters, based on unit selected above.

>= 1

content.type.nearest.options

Key

Type

Optionality

Description

Constraints

limit

number

OPTIONAL

How many nearest point from each content.points is selected. The default value is 1.

>= 1

content.pointQueries.properties

An array of properties to be computed for each point. The properties can be attributes (e.g. name of nearest bank name) and/or metric (e.g. number of POIs). The syntax of properties is identical to that for Dwh Queries.

If there are both attribute(s) and metric(s) present in the query, the metric(s) result is grouped by attribute(s). See example query transactions_foot_20_min_by_shop above.

content.pointQueries.filterBy

Each pointQuery can have defined additonal filters. The syntax of filters is again identical to that for Dwh Queries. See example of usage filterBy in transactions_foot_20_min_by_shop query above.

content.pointQueries.having

The results of metrics computed in the point query can be filtered by a having filter.

Property types reference

The properties array in each point query defines what values to compute. Each property object must have an id (used as the key in the response) and a type. The following property types are supported:

property

Returns a raw attribute value from the dataset. When combined with a metric or aggregation function, it acts as a group-by dimension โ€” the metric result is broken down by the distinct values of this property.

CODE
{
  "id": "shop_name",
  "type": "property",
  "value": "shops.name"
}

metric

References an existing metric definition in the project via its REST API URL. This is useful when you have prebuilt metrics and want to reuse them in point queries without re-declaring the aggregation logic.

CODE
{
  "id": "turnover_by_10_minutes_walk",
  "type": "metric",
  "metric": "/rest/projects/{projectId}/md/metrics?name=turnover_metric"
}

Alternatively to the prebuilt metric, you can use a in-place function(s), see rerefence documentation Metrics. He is just a quick introduction of one of supported functions - function_distance which is useful for bulkPointQuery.

function_distance

Returns the straight-line distance (in meters) from the input point to each matched record. This type is used exclusively in nearest queries.

CODE
{
  "id": "distance_to_nearest",
  "type": "function_distance"
}

function_avg

Computes the average of a nested function across all matched records. Typically combined with function_distance inside nearest queries to calculate the average distance to N closest points.

CODE
{
  "id": "avg_distance_to_3_nearest",
  "type": "function_avg",
  "content": [
    {
      "type": "function_distance"
    }
  ]
}

Example: Nearest point query

The nearest query type finds the N closest points from a dataset to each input point. Use options.limit to control how many nearest points are returned (defaults to 1).

This example finds the 2 nearest partner shops for each input point, returning the shop ID, name, and distance:

Request body (content.pointQueries item):

CODE
{
  "queryId": "nearest_partner_shops",
  "type": "nearest",
  "options": {
    "limit": 2
  },
  "dataset": "shops",
  "properties": [
    {
      "id": "shop_id",
      "type": "property",
      "value": "shops.shop_id"
    },
    {
      "id": "partner_name",
      "type": "property",
      "value": "shops.name"
    },
    {
      "id": "distance_to_nearest_partner",
      "type": "function_distance"
    }
  ],
  "filterBy": [
    {
      "operator": "eq",
      "property": "shops.partner",
      "value": "yes"
    }
  ]
}

Response โ€” the content array contains exactly 2 items (matching options.limit), each representing one nearest partner shop:

CODE
{
  "queryId": "nearest_partner_shops",
  "content": [
    {
      "shop_id": 2,
      "partner_name": "Partner: Alum Rock Road",
      "distance_to_nearest_partner": 2795.59
    },
    {
      "shop_id": 7,
      "partner_name": "Partner: Coventry Road - Digbeth",
      "distance_to_nearest_partner": 2765.79
    }
  ]
}

Example: Metric breakdown by property (grouping)

When a point query includes both a metric (or aggregation function) and a property, the metric result is automatically grouped by the distinct values of that property. This is analogous to a SQL GROUP BY clause.

This example computes the sum of POIs within a 200m air-distance radius, broken down by POI type:

Request body (content.pointQueries item):

CODE
{
  "queryId": "pois_by_type",
  "type": "isochrone",
  "options": {
    "profile": "air",
    "unit": "distance",
    "amount": 200
  },
  "dataset": "poi_dwh",
  "properties": [
    {
      "id": "location_pois_sum_metric",
      "type": "metric",
      "metric": "/rest/projects/{projectId}/md/metrics?name=location_pois_sum_metric"
    },
    {
      "id": "poi_type",
      "type": "property",
      "value": "poi_dwh.type_en_label"
    }
  ]
}

Response โ€” the content array contains one item per distinct value of poi_dwh.type_en_label:

CODE
{
  "queryId": "pois_by_type",
  "content": [
    { "location_pois_sum_metric": 39, "poi_type": "Accommodation" },
    { "location_pois_sum_metric": 335, "poi_type": "Banks and ATMs" },
    { "location_pois_sum_metric": 210, "poi_type": "Education" },
    { "location_pois_sum_metric": 70, "poi_type": "Food" },
    { "location_pois_sum_metric": 186.25, "poi_type": "Healthcare" },
    { "location_pois_sum_metric": 75, "poi_type": "Office" },
    { "location_pois_sum_metric": 269, "poi_type": "Residence" },
    { "location_pois_sum_metric": 252, "poi_type": "Shopping" },
    { "location_pois_sum_metric": 50, "poi_type": "Transportation" }
  ]
}

Example: Average distance to N nearest points

Combine function_avg with function_distance inside a nearest query to compute the average distance to the N closest points. This is useful for evaluating how well a location is served by nearby facilities.

Request body (content.pointQueries item):

CODE
{
  "queryId": "avg_distance_3_nearest_partners",
  "type": "nearest",
  "options": {
    "limit": 3
  },
  "dataset": "shops",
  "properties": [
    {
      "id": "avg_distance_to_3_nearest_partners",
      "type": "function_avg",
      "content": [
        {
          "type": "function_distance"
        }
      ]
    }
  ],
  "filterBy": [
    {
      "operator": "eq",
      "property": "shops.partner",
      "value": "yes"
    }
  ]
}

Response โ€” a single aggregated value:

CODE
{
  "queryId": "avg_distance_3_nearest_partners",
  "content": [
    {
      "avg_distance_to_3_nearest_partners": 4305.14
    }
  ]
}

Tip: Omitting filterBy searches the entire dataset. Compare filtered vs. unfiltered nearest queries to understand the competitive landscape โ€” e.g., average distance to partner shops vs. all shops.


Example: Combining isochrone and nearest queries

A single Bulk point query request can combine multiple isochrone and nearest queries. Each query is identified by its queryId and executed independently. This is the recommended approach for location planning scenarios where you need to evaluate multiple aspects of a location in a single API call.

The following example evaluates two input points by computing:

  1. Turnover within a 10-minute walk (isochrone, foot profile)

  2. Number of customers within a 15-minute drive (isochrone, car profile)

  3. The 2 nearest partner shops with distances (nearest, limit 2)

  4. Average distance to the 3 nearest partner shops (nearest + function_avg)

  5. Average distance to the 3 nearest shops overall (nearest + function_avg, no filter)

Full request

CODE
{
  "type": "bulkPointQuery",
  "projectId": "ihhbdivmpgif8o4b",
  "content": {
    "points": [
      {
        "lat": 52.4805067,
        "lng": -1.8939561
      },
      {
        "id": "optional_point_id",
        "lat": 53.48,
        "lng": -2.89
      }
    ],
    "pointQueries": [
      {
        "queryId": "turnover_10min_walk",
        "type": "isochrone",
        "options": { "profile": "foot", "unit": "time", "amount": "10" },
        "dataset": "shops",
        "properties": [
          {
            "id": "turnover_by_10_minutes_walk",
            "type": "metric",
            "metric": "/rest/projects/ihhbdivmpgif8o4b/md/metrics?name=turnover_metric"
          }
        ]
      },
      {
        "queryId": "customers_15min_car",
        "type": "isochrone",
        "options": { "profile": "car", "unit": "time", "amount": "15" },
        "dataset": "shops",
        "properties": [
          {
            "id": "customers_in_15_minutes_car",
            "type": "metric",
            "metric": "/rest/projects/ihhbdivmpgif8o4b/md/metrics?name=customers_metric"
          }
        ]
      },
      {
        "queryId": "nearest_2_partner_shops",
        "type": "nearest",
        "options": { "limit": 2 },
        "dataset": "shops",
        "properties": [
          { "id": "shop_id", "type": "property", "value": "shops.shop_id" },
          { "id": "partner_name", "type": "property", "value": "shops.name" },
          { "id": "distance_to_nearest_partner", "type": "function_distance" }
        ],
        "filterBy": [
          { "operator": "eq", "property": "shops.partner", "value": "yes" }
        ]
      },
      {
        "queryId": "avg_dist_3_nearest_partners",
        "type": "nearest",
        "options": { "limit": 3 },
        "dataset": "shops",
        "properties": [
          {
            "id": "avg_distance_to_3_nearest_partners",
            "type": "function_avg",
            "content": [ { "type": "function_distance" } ]
          }
        ],
        "filterBy": [
          { "operator": "eq", "property": "shops.partner", "value": "yes" }
        ]
      },
      {
        "queryId": "avg_dist_3_nearest_all",
        "type": "nearest",
        "options": { "limit": 3 },
        "dataset": "shops",
        "properties": [
          {
            "id": "avg_distance_to_3_nearest_all_stores",
            "type": "function_avg",
            "content": [ { "type": "function_distance" } ]
          }
        ]
      }
    ]
  }
}

Full response

CODE
{
  "id": "30c0e7af-5f2f-43fb-b2f5-50567b31e38d",
  "type": "bulkPointQuery",
  "status": "SUCCEEDED",
  "result": {
    "processedPoints": [
      {
        "lat": 52.4805067,
        "lng": -1.8939561,
        "content": [
          {
            "queryId": "turnover_10min_walk",
            "content": [{ "turnover_by_10_minutes_walk": 2422036.65 }]
          },
          {
            "queryId": "customers_15min_car",
            "content": [{ "customers_in_15_minutes_car": 37775 }]
          },
          {
            "queryId": "nearest_2_partner_shops",
            "content": [
              { "shop_id": 2, "partner_name": "Partner: Alum Rock Road", "distance_to_nearest_partner": 2795.59 },
              { "shop_id": 7, "partner_name": "Partner: Coventry Road - Digbeth", "distance_to_nearest_partner": 2765.79 }
            ]
          },
          {
            "queryId": "avg_dist_3_nearest_partners",
            "content": [{ "avg_distance_to_3_nearest_partners": 4305.14 }]
          },
          {
            "queryId": "avg_dist_3_nearest_all",
            "content": [{ "avg_distance_to_3_nearest_all_stores": 2011.03 }]
          }
        ]
      },
      {
        "id": "optional_point_id",
        "lat": 53.48,
        "lng": -2.89,
        "content": [
          {
            "queryId": "turnover_10min_walk",
            "content": [{}]
          },
          {
            "queryId": "customers_15min_car",
            "content": [{ "customers_in_15_minutes_car": 0 }]
          },
          {
            "queryId": "nearest_2_partner_shops",
            "content": [
              { "shop_id": 2, "partner_name": "Partner: Alum Rock Road", "distance_to_nearest_partner": 129993.04 },
              { "shop_id": 5, "partner_name": "Partner: Rood End Rd / Tat Bank Rd", "distance_to_nearest_partner": 124709.99 }
            ]
          },
          {
            "queryId": "avg_dist_3_nearest_partners",
            "content": [{ "avg_distance_to_3_nearest_partners": 128879.93 }]
          },
          {
            "queryId": "avg_dist_3_nearest_all",
            "content": [{ "avg_distance_to_3_nearest_all_stores": 127935.91 }]
          }
        ]
      }
    ],
    "consumedCredits": 0
  }
}

Key observations

  • Empty results: When no data matches (e.g., no shops within a 10-minute walk), the content array contains an empty object {}. Your client code should handle this gracefully.

  • Distance units: function_distance always returns distance in meters.

  • Nearest without filter: Omitting filterBy searches the entire dataset. Compare avg_dist_3_nearest_partners (filtered) vs. avg_dist_3_nearest_all (unfiltered) to understand the difference.

  • Credits: The consumedCredits field indicates how many API credits were consumed by the query execution.

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.