Concave hull approximation from point coordinates
A convex hull is useful, but it often wraps a scattered point set too loosely. This concave hull approximation starts with that reliable outer boundary and then refines long edges with unused interior points until the requested length threshold can be met or no valid insertion remains.
Run — free
The deterministic result includes the ordered boundary, edge lengths, perimeter, area, and a count of edges that could not be shortened, making the approximation easy to inspect rather than presenting it as an exact geometric answer.
Choose coordinates and a meaningful threshold
Provide at least three planar points as numeric x and y coordinates, together with a positive edge-length threshold. Coordinates may represent projected map positions, drawing units, measurements, or any other two-dimensional Cartesian system. They should share one scale: do not mix longitude degrees with distances in meters and expect the threshold to retain a physical meaning. A smaller threshold asks the algorithm to follow the available points more closely, while a larger threshold preserves more of the convex outline. The threshold is a target, not a guarantee, because a sparse point set may contain no safe interior point capable of splitting a particular boundary edge. Duplicate coordinates are ignored. At least three unique, non-collinear points must remain, since a polygon cannot be formed from a line. For geographic latitude and longitude data, project the coordinates appropriately before using a threshold expressed in a distance unit. The tool performs planar geometry and does not apply a spherical or ellipsoidal Earth model. Start with a threshold near the spacing you consider a meaningful gap, then compare the returned boundary with the original points before using it downstream.
Understand how boundary refinement works
The algorithm first computes a deterministic convex hull with the monotone-chain method. It then examines boundary edges longer than the supplied threshold, longest first. For each such edge, it searches unused points that lie inside the current polygon and can replace the edge with two shorter segments without crossing another boundary segment. The candidate that minimizes the longer replacement segment is selected, with stable tie breaking based on total replacement length and original input order. After an insertion, edge selection begins again because the polygon has changed. Refinement stops when every edge is within the target or when no remaining point can safely improve any long edge. This construction deliberately favors predictable, explainable output over claiming to solve the many competing definitions of an exact concave hull. It never invents vertices, moves coordinates, uses random sampling, or calls a remote service. The returned hull is an open ordered vertex list whose final vertex connects back to the first. Edge lengths use that same cyclic order. Collinear outer points may be omitted unless one later becomes a useful refinement vertex.
Inspect limitations before relying on the polygon
Review unresolved_long_edges whenever the threshold matters operationally. A value of zero means every returned boundary edge meets the requested target; a positive value means the available point geometry could not satisfy it without a crossing, an outward move, or a replacement that failed to shorten the edge. That is useful evidence, not a hidden failure. The approximation can also produce a boundary that differs from results generated by alpha shapes, k-nearest-neighbor hulls, triangulation filters, or domain-specific shoreline methods. Compare methods when topology carries legal, scientific, or safety consequences. The reported area and perimeter belong to the returned planar polygon and use the coordinate system supplied by the caller. They are not geodesic measurements. Plot the ordered hull over the source points to check whether the chosen threshold captures the intended clusters and indentations. If the boundary remains too broad, add representative points or reduce the threshold; if it becomes overly detailed, raise the threshold. The capability accepts at most 500 input items so its repeated intersection checks remain bounded and suitable for interactive browser execution as well as deterministic API automation.
What you can do with it
Outline a sampled site
Create an inspectable boundary around projected survey or sensor points while retaining visible inward structure.
Prepare a map preview
Turn a moderate point collection into an ordered polygon for quick visualization before applying a specialized GIS workflow.
Compare boundary sensitivity
Run several thresholds and compare area, perimeter, inserted vertices, and unresolved edges to select a useful approximation.
FAQ
Is this an exact concave hull?
No. Concave hull has several definitions; this method deterministically refines long convex-hull edges with safe interior points.
What does it cost?
Each API request costs $0.002. The same deterministic computation can run in the browser.
Does every edge end below the threshold?
Not necessarily. The unresolved_long_edges field reports edges that could not be safely shortened using the available points.
Can I use latitude and longitude directly?
The calculation will run, but distances and areas remain angular planar values. Project the data first when physical units matter.
Is the first hull point repeated at the end?
No. The hull is an open ordered list; treat the last point as connected back to the first.
How are duplicate or collinear points handled?
Exact duplicate coordinates are ignored. A fully collinear unique set is rejected because it cannot define a polygon.
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/geo/concave-hull-approx \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"points":[{"x":0,"y":0},{"x":4,"y":0},{"x":4,"y":4},{"x":2,"y":1},{"x":0,"y":4}],"threshold":3}'const res = await fetch("https://api.kit.forhosting.com/geo/concave-hull-approx", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"points": [
{
"x": 0,
"y": 0
},
{
"x": 4,
"y": 0
},
{
"x": 4,
"y": 4
},
{
"x": 2,
"y": 1
},
{
"x": 0,
"y": 4
}
],
"threshold": 3
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/geo/concave-hull-approx",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"points": [
{
"x": 0,
"y": 0
},
{
"x": 4,
"y": 0
},
{
"x": 4,
"y": 4
},
{
"x": 2,
"y": 1
},
{
"x": 0,
"y": 4
}
],
"threshold": 3
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/geo/concave-hull-approx", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"points":[{"x":0,"y":0},{"x":4,"y":0},{"x":4,"y":4},{"x":2,"y":1},{"x":0,"y":4}],"threshold":3}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"points":[{"x":0,"y":0},{"x":4,"y":0},{"x":4,"y":4},{"x":2,"y":1},{"x":0,"y":4}],"threshold":3}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/geo/concave-hull-approx", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"points": [
{
"x": 0,
"y": 0
},
{
"x": 4,
"y": 0
},
{
"x": 4,
"y": 4
},
{
"x": 2,
"y": 1
},
{
"x": 0,
"y": 4
}
],
"threshold": 3
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "geo.concave_hull_approx",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Limits
max_items | 500 |
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |