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:
Submit a bulkPointQuery request as
POST /rest/jobsendpoint. The accepted request returns a 202 HTTP status and the URL of created job (eg./rest/jobs/db00de52-5ac8-0dc8-43d8-1cc162605007?type=bulkPointQuery).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 fromRUNNINGtoSUCCEDDEDand 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
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:
Sum of transactions (property
baskets.amount) in 20 minutes foot-walking area from each ofpointsSum of transactions in the same area but grouped by shops (property
shops.name). The shops must match attribute filtershop.partner = 'no'.
Expected result status is 202 and the following JSON response:
{
"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:
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:
{
"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:
{
"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 |
|---|---|---|---|---|
| string | REQUIRED | asynchronous job execution type |
|
| string | REQUIRED | id of CleverMaps project for execution the Bulk point query |
|
| object | REQUIRED | Bulk point query request |
|
content
Key | Type | Optionality | Description | Constraints |
|---|---|---|---|---|
| array | REQUIRED | list of coordinates objects. | |
| array | REQUIRED | list of queries to be executed on each point from array |
|
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 |
|---|---|---|---|---|
| string | OPTIONAL | Optional identifier of the point. The identifier is present in Bulk point query result and can simplify the parsing of response. | |
| number | REQUIRED | latitude coordinate of the point | |
| number | REQUIRED | longitude coordinate of the point |
content.pointQueries
An array of objects described bellow.
Key | Type | Optionality | Description | Constraints |
|---|---|---|---|---|
| string | REQUIRED | an identifier of each query, it is used for grouping query results in the bulkPointQuery response | |
| string | REQUIRED | a type of query. There are suported two types of queries:
|
|
| object | REQUIRED | additional paremeters for query type. See more details bellow. | |
| string | REQUIRED | name of geometryPoint dataset for executiond the query | |
| 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,bikeandfootprofiles)or a direct distance (circle) defined by meters from a point (see
airprofile).
The definition of isochrone parameters is in options object:
Key | Type | Optionality | Description | Constraints |
|---|---|---|---|---|
| string | REQUIRED | The type of isochrone profile |
|
| string | REQUIRED | Unit of computed isochrone |
|
| number | REQUIRED | Number of minutes or meters, based on | >= 1 |
content.type.nearest.options
Key | Type | Optionality | Description | Constraints |
|---|---|---|---|---|
| number | OPTIONAL | How many nearest point from each | >= 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.
{
"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.
{
"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.
{
"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.
{
"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):
{
"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:
{
"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):
{
"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:
{
"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):
{
"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:
{
"queryId": "avg_distance_3_nearest_partners",
"content": [
{
"avg_distance_to_3_nearest_partners": 4305.14
}
]
}
Tip: Omitting
filterBysearches 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:
Turnover within a 10-minute walk (
isochrone, foot profile)Number of customers within a 15-minute drive (
isochrone, car profile)The 2 nearest partner shops with distances (
nearest, limit 2)Average distance to the 3 nearest partner shops (
nearest+function_avg)Average distance to the 3 nearest shops overall (
nearest+function_avg, no filter)
Full request
{
"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
{
"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
contentarray contains an empty object{}. Your client code should handle this gracefully.Distance units:
function_distancealways returns distance in meters.Nearest without filter: Omitting
filterBysearches the entire dataset. Compareavg_dist_3_nearest_partners(filtered) vs.avg_dist_3_nearest_all(unfiltered) to understand the difference.Credits: The
consumedCreditsfield indicates how many API credits were consumed by the query execution.