PostGIS 3.7.0dev-r@@SVN_REVISION@@
Loading...
Searching...
No Matches
gserialized_spgist_nd.c
Go to the documentation of this file.
1/*****************************************************************************
2 *
3 * gserialized_spgist_nd.c
4 * SP-GiST implementation of 2N-dimensional oct-tree over GIDX boxes
5 *
6 * This module provides SP-GiST implementation for N-dimensional boxes using
7 * oct-tree analogy in 2N-dimensional space. SP-GiST doesn't allow indexing
8 * of overlapping objects. We make N-dimensional objects never-overlapping
9 * in 2N-dimensional space. This technique has some benefits compared to
10 * traditional R-Tree which is implemented as GiST. The performance tests
11 * reveal that this technique especially beneficial with too much overlapping
12 * objects, so called "spaghetti data".
13 *
14 * Unlike the original oct-tree, we are splitting the tree 2N times in
15 * N-dimensional space. For example, if N = 3 then it is easier to imagine
16 * it as splitting space three times into 3:
17 *
18 * | | | |
19 * | | | |
20 * | -----+----- | -----+-----
21 * | | | |
22 * | | | |
23 * -------------+------------- -+- -------------+-------------
24 * | |
25 * | |
26 * | |
27 * | |
28 * | |
29 * FRONT BACK
30 *
31 * We are using GIDX data type as the prefix, but we are treating them
32 * as points in 2N-dimensional space, because GIDX boxes are not enough
33 * to represent the octant boundaries in ND space. They however are
34 * sufficient to point out the additional boundaries of the next
35 * octant.
36 *
37 * We are using traversal values provided by SP-GiST to calculate and
38 * to store the bounds of the octants, while traversing into the tree.
39 * Traversal value has all the boundaries in the ND space, and is
40 * capable of transferring the required boundaries to the following
41 * traversal values. In conclusion, three things are necessary
42 * to calculate the next traversal value:
43 *
44 * (1) the traversal value of the parent
45 * (2) the octant of the current node
46 * (3) the prefix of the current node
47 *
48 * If we visualize them on our simplified drawing (see the drawing above);
49 * transferred boundaries of (1) would be the outer axis, relevant part
50 * of (2) would be the up range_y part of the other axis, and (3) would be
51 * the inner axis.
52 *
53 * For example, consider the case of overlapping. When recursion
54 * descends deeper and deeper down the tree, all octants in
55 * the current node will be checked for overlapping. The boundaries
56 * will be re-calculated for all octants. Overlap check answers
57 * the question: can any box from this octant overlap with the given
58 * box? If yes, then this octant will be walked. If no, then this
59 * octant will be skipped.
60 *
61 * This method provides restrictions for minimum and maximum values of
62 * every dimension of every corner of the box on every level of the tree
63 * except the root. For the root node, we are setting the boundaries
64 * that we don't yet have as infinity.
65 *
66 * Portions Copyright (c) 2018, Esteban Zimanyi, Arthur Lesuisse,
67 * Université Libre de Bruxelles
68 * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group
69 * Portions Copyright (c) 1994, Regents of the University of California
70 *
71 *****************************************************************************/
72
74#include "lwgeom_pg.h"
75#include "gserialized_gist.h"
76
77/* Maximum number of children nodes given that GIDX_MAX_DIM = 4 */
78#define GIDX_MAX_NODES 256
79
80/*
81 * ND SP-GiST prototypes
82 */
83
84Datum gserialized_spgist_config_nd(PG_FUNCTION_ARGS);
85Datum gserialized_spgist_choose_nd(PG_FUNCTION_ARGS);
86Datum gserialized_spgist_picksplit_nd(PG_FUNCTION_ARGS);
87Datum gserialized_spgist_inner_consistent_nd(PG_FUNCTION_ARGS);
88Datum gserialized_spgist_leaf_consistent_nd(PG_FUNCTION_ARGS);
89Datum gserialized_spgist_compress_nd(PG_FUNCTION_ARGS);
90
91/* Structure storing the n-dimensional bounding box */
92
93typedef struct
94{
95 GIDX *left;
96 GIDX *right;
97} CubeGIDX;
98
99/*
100 * Comparator for qsort
101 *
102 * We don't need to use the floating point macros in here, because this
103 * is only going to be used in a place to effect the performance
104 * of the index, not the correctness.
105 */
106static int
107compareFloats(const void *a, const void *b)
108{
109 float x = *(float *)a;
110 float y = *(float *)b;
111
112 if (x == y)
113 return 0;
114 return (x > y) ? 1 : -1;
115}
116
117/*
118 * Calculate the octant
119 *
120 * The octant is a 16-bit unsigned integer with 8 bits in use. The function
121 * accepts 2 GIDX as input. The 8 bits are set by comparing a corner of the
122 * box. This makes 256 octants in total.
123 */
124static uint16_t
125getOctant(const GIDX *centroid, const GIDX *inBox)
126{
127 uint16_t octant = 0, dim = 0x01;
128 int ndims, i;
129
130 ndims = Min(GIDX_NDIMS(centroid), GIDX_NDIMS(inBox));
131
132 for (i = 0; i < ndims; i++)
133 {
134 /* If the missing dimension was not padded with -+FLT_MAX */
135 if (GIDX_GET_MAX(centroid, i) != FLT_MAX && GIDX_GET_MAX(inBox, i) != FLT_MAX)
136 {
137 if (GIDX_GET_MAX(inBox, i) > GIDX_GET_MAX(centroid, i))
138 octant |= dim;
139 dim = dim << 1;
140 if (GIDX_GET_MIN(inBox, i) > GIDX_GET_MIN(centroid, i))
141 octant |= dim;
142 dim = dim << 1;
143 }
144 }
145
146 return octant;
147}
148
149/*
150 * Initialize the traversal value
151 *
152 * In the beginning, we don't have any restrictions. We have to
153 * initialize the struct to cover the whole ND space.
154 */
155static CubeGIDX *
156initCubeBox(int ndims)
157{
158 CubeGIDX *cube_box = (CubeGIDX *)palloc(sizeof(CubeGIDX));
159 GIDX *left = (GIDX *)palloc(GIDX_SIZE(ndims));
160 GIDX *right = (GIDX *)palloc(GIDX_SIZE(ndims));
161 int i;
162
163 SET_VARSIZE(left, GIDX_SIZE(ndims));
164 SET_VARSIZE(right, GIDX_SIZE(ndims));
165 cube_box->left = left;
166 cube_box->right = right;
167
168 for (i = 0; i < ndims; i++)
169 {
170 GIDX_SET_MIN(cube_box->left, i, -1 * FLT_MAX);
171 GIDX_SET_MAX(cube_box->left, i, FLT_MAX);
172 GIDX_SET_MIN(cube_box->right, i, -1 * FLT_MAX);
173 GIDX_SET_MAX(cube_box->right, i, FLT_MAX);
174 }
175
176 return cube_box;
177}
178
179/*
180 * Calculate the next traversal value
181 *
182 * All centroids are bounded by CubeGIDX, but SP-GiST only keeps
183 * boxes. When we are traversing the tree, we must calculate CubeGIDX,
184 * using centroid and octant.
185 */
186static CubeGIDX *
187nextCubeBox(CubeGIDX *cube_box, GIDX *centroid, uint16_t octant)
188{
189 int ndims = GIDX_NDIMS(centroid), i;
190 CubeGIDX *next_cube_box = (CubeGIDX *)palloc(sizeof(CubeGIDX));
191 GIDX *left = (GIDX *)palloc(GIDX_SIZE(ndims));
192 GIDX *right = (GIDX *)palloc(GIDX_SIZE(ndims));
193 uint16_t dim = 0x01;
194
195 memcpy(left, cube_box->left, VARSIZE(cube_box->left));
196 memcpy(right, cube_box->right, VARSIZE(cube_box->right));
197 next_cube_box->left = left;
198 next_cube_box->right = right;
199
200 for (i = 0; i < ndims; i++)
201 {
202 if (GIDX_GET_MAX(cube_box->left, i) != FLT_MAX && GIDX_GET_MAX(centroid, i) != FLT_MAX)
203 {
204 if (octant & dim)
205 GIDX_GET_MIN(next_cube_box->right, i) = GIDX_GET_MAX(centroid, i);
206 else
207 GIDX_GET_MAX(next_cube_box->right, i) = GIDX_GET_MAX(centroid, i);
208
209 dim = dim << 1;
210
211 if (octant & dim)
212 GIDX_GET_MIN(next_cube_box->left, i) = GIDX_GET_MIN(centroid, i);
213 else
214 GIDX_GET_MAX(next_cube_box->left, i) = GIDX_GET_MIN(centroid, i);
215
216 dim = dim << 1;
217 }
218 }
219
220 return next_cube_box;
221}
222
223/* Can any cube from cube_box overlap with query? */
224static bool
225overlapND(CubeGIDX *cube_box, GIDX *query)
226{
227 int i, ndims;
228 bool result = true;
229
230 ndims = Min(GIDX_NDIMS(cube_box->left), GIDX_NDIMS(query));
231
232 for (i = 0; i < ndims; i++)
233 {
234 /* If the missing dimension was not padded with -+FLT_MAX */
235 if (GIDX_GET_MAX(cube_box->left, i) != FLT_MAX && GIDX_GET_MAX(query, i) != FLT_MAX)
236 result &= (GIDX_GET_MIN(cube_box->left, i) <= GIDX_GET_MAX(query, i)) &&
237 (GIDX_GET_MAX(cube_box->right, i) >= GIDX_GET_MIN(query, i));
238 }
239 return result;
240}
241
242/* Can any cube from cube_box contain query? */
243static bool
244containND(CubeGIDX *cube_box, GIDX *query)
245{
246 int i, ndims;
247 bool result = true;
248
249 ndims = Min(GIDX_NDIMS(cube_box->left), GIDX_NDIMS(query));
250
251 for (i = 0; i < ndims; i++)
252 {
253 /* If the missing dimension was not padded with -+FLT_MAX */
254 if (GIDX_GET_MAX(cube_box->left, i) != FLT_MAX && GIDX_GET_MAX(query, i) != FLT_MAX)
255 result &= (GIDX_GET_MAX(cube_box->right, i) >= GIDX_GET_MAX(query, i)) &&
256 (GIDX_GET_MIN(cube_box->left, i) <= GIDX_GET_MIN(query, i));
257 }
258 return result;
259}
260
261/*
262 * SP-GiST config function
263 */
264
266
267PGDLLEXPORT Datum gserialized_spgist_config_nd(PG_FUNCTION_ARGS)
268{
269 spgConfigOut *cfg = (spgConfigOut *)PG_GETARG_POINTER(1);
270 Oid boxoid = InvalidOid;
271 postgis_initialize_cache();
272 boxoid = postgis_oid(GIDXOID);
273
274 cfg->prefixType = boxoid;
275 cfg->labelType = VOIDOID; /* We don't need node labels. */
276 cfg->leafType = boxoid;
277 cfg->canReturnData = false;
278 cfg->longValuesOK = false;
279
280 PG_RETURN_VOID();
281}
282
283/*
284 * SP-GiST choose function
285 */
286
288
289PGDLLEXPORT Datum gserialized_spgist_choose_nd(PG_FUNCTION_ARGS)
290{
291 spgChooseIn *in = (spgChooseIn *)PG_GETARG_POINTER(0);
292 spgChooseOut *out = (spgChooseOut *)PG_GETARG_POINTER(1);
293 GIDX *centroid = (GIDX *)DatumGetPointer(in->prefixDatum), *box = (GIDX *)DatumGetPointer(in->leafDatum);
294
295 out->resultType = spgMatchNode;
296 out->result.matchNode.restDatum = PointerGetDatum(box);
297
298 /* nodeN will be set by core, when allTheSame. */
299 if (!in->allTheSame)
300 out->result.matchNode.nodeN = getOctant(centroid, box);
301
302 PG_RETURN_VOID();
303}
304
305/*
306 * SP-GiST pick-split function
307 *
308 * It splits a list of boxes into octants by choosing a central ND
309 * point as the median of the coordinates of the boxes.
310 */
312
313PGDLLEXPORT Datum gserialized_spgist_picksplit_nd(PG_FUNCTION_ARGS)
314{
315 spgPickSplitIn *in = (spgPickSplitIn *)PG_GETARG_POINTER(0);
316 spgPickSplitOut *out = (spgPickSplitOut *)PG_GETARG_POINTER(1);
317 GIDX *centroid;
318 float *lowXs, *highXs;
319 int ndims, maxdims = -1, count[GIDX_MAX_DIM], median, dim, tuple;
320
321 for (dim = 0; dim < GIDX_MAX_DIM; dim++)
322 count[dim] = 0;
323
324 lowXs = palloc(sizeof(float) * in->nTuples * GIDX_MAX_DIM),
325 highXs = palloc(sizeof(float) * in->nTuples * GIDX_MAX_DIM);
326
327 /* Calculate maxdims median of all ND coordinates */
328 for (tuple = 0; tuple < in->nTuples; tuple++)
329 {
330 const GIDX *box = (const GIDX *)DatumGetPointer(in->datums[tuple]);
331 ndims = GIDX_NDIMS(box);
332 if (maxdims < ndims)
333 maxdims = ndims;
334 for (dim = 0; dim < ndims; dim++)
335 {
336 /* If the missing dimension was not padded with -+FLT_MAX */
337 if (GIDX_GET_MAX(box, dim) != FLT_MAX)
338 {
339 lowXs[dim * in->nTuples + count[dim]] = GIDX_GET_MIN(box, dim);
340 highXs[dim * in->nTuples + count[dim]] = GIDX_GET_MAX(box, dim);
341 count[dim]++;
342 }
343 }
344 }
345
346 for (dim = 0; dim < maxdims; dim++)
347 {
348 qsort(&lowXs[dim * in->nTuples], count[dim], sizeof(float), compareFloats);
349 qsort(&highXs[dim * in->nTuples], count[dim], sizeof(float), compareFloats);
350 }
351
352 centroid = (GIDX *)palloc(GIDX_SIZE(maxdims));
353 SET_VARSIZE(centroid, GIDX_SIZE(maxdims));
354
355 for (dim = 0; dim < maxdims; dim++)
356 {
357 median = count[dim] / 2;
358 GIDX_SET_MIN(centroid, dim, lowXs[dim * in->nTuples + median]);
359 GIDX_SET_MAX(centroid, dim, highXs[dim * in->nTuples + median]);
360 }
361
362 /* Fill the output */
363 out->hasPrefix = true;
364 out->prefixDatum = PointerGetDatum(gidx_copy(centroid));
365
366 out->nNodes = 0x01 << (2 * maxdims);
367 out->nodeLabels = NULL; /* We don't need node labels. */
368
369 out->mapTuplesToNodes = palloc(sizeof(int) * in->nTuples);
370 out->leafTupleDatums = palloc(sizeof(Datum) * in->nTuples);
371
372 /*
373 * Assign ranges to corresponding nodes according to octants relative to
374 * the "centroid" range
375 */
376 for (tuple = 0; tuple < in->nTuples; tuple++)
377 {
378 GIDX *box = (GIDX *)DatumGetPointer(in->datums[tuple]);
379 uint16_t octant = getOctant(centroid, box);
380
381 out->leafTupleDatums[tuple] = PointerGetDatum(box);
382 out->mapTuplesToNodes[tuple] = octant;
383 }
384
385 pfree(lowXs);
386 pfree(highXs);
387
388 PG_RETURN_VOID();
389}
390
391/*
392 * SP-GiST inner consistent function
393 */
395
396PGDLLEXPORT Datum gserialized_spgist_inner_consistent_nd(PG_FUNCTION_ARGS)
397{
398 spgInnerConsistentIn *in = (spgInnerConsistentIn *)PG_GETARG_POINTER(0);
399 spgInnerConsistentOut *out = (spgInnerConsistentOut *)PG_GETARG_POINTER(1);
400 MemoryContext old_ctx;
401 CubeGIDX *cube_box;
402 int *nodeNumbers, i, j;
403 void **traversalValues;
404 char gidxmem[GIDX_MAX_SIZE];
405 GIDX *centroid, *query_gbox_index = (GIDX *)gidxmem;
406
407 POSTGIS_DEBUG(4, "[SPGIST] 'inner consistent' function called");
408
409 if (in->allTheSame)
410 {
411 /* Report that all nodes should be visited */
412 out->nNodes = in->nNodes;
413 out->nodeNumbers = (int *)palloc(sizeof(int) * in->nNodes);
414 for (i = 0; i < in->nNodes; i++)
415 out->nodeNumbers[i] = i;
416
417 PG_RETURN_VOID();
418 }
419
420 /*
421 * We switch memory context, because we want to allocate memory for new
422 * traversal values (next_cube_box) and pass these pieces of memory to
423 * further call of this function.
424 */
425 old_ctx = MemoryContextSwitchTo(in->traversalMemoryContext);
426
427 centroid = (GIDX *)DatumGetPointer(in->prefixDatum);
428
429 /*
430 * We are saving the traversal value or initialize it an unbounded one, if
431 * we have just begun to walk the tree.
432 */
433 if (in->traversalValue)
434 cube_box = in->traversalValue;
435 else
436 cube_box = initCubeBox(GIDX_NDIMS(centroid));
437
438 /* Allocate enough memory for nodes */
439 out->nNodes = 0;
440 nodeNumbers = (int *)palloc(sizeof(int) * in->nNodes);
441 traversalValues = (void **)palloc(sizeof(void *) * in->nNodes);
442
443 for (i = 0; i < in->nNodes; i++)
444 {
445 CubeGIDX *next_cube_box = nextCubeBox(cube_box, centroid, (uint16_t)i);
446 bool flag = true;
447
448 for (j = 0; j < in->nkeys; j++)
449 {
450 StrategyNumber strategy = in->scankeys[j].sk_strategy;
451 Datum query = in->scankeys[j].sk_argument;
452
453 /* Quick sanity check on query argument. */
454 if (DatumGetPointer(query) == NULL)
455 {
456 POSTGIS_DEBUG(4, "[SPGIST] null query pointer (!?!)");
457 flag = false;
458 break;
459 }
460
461 /* Null box should never make this far. */
462 if (gserialized_datum_get_gidx_p(query, query_gbox_index) == LW_FAILURE)
463 {
464 POSTGIS_DEBUG(4, "[SPGIST] null query_gbox_index!");
465 flag = false;
466 break;
467 }
468
469 switch (strategy)
470 {
473 flag = overlapND(next_cube_box, query_gbox_index);
474 break;
475
478 flag = containND(next_cube_box, query_gbox_index);
479 break;
480
481 default:
482 elog(ERROR, "unrecognized strategy: %d", strategy);
483 }
484
485 /* If any check is failed, we have found our answer. */
486 if (!flag)
487 break;
488 }
489
490 if (flag)
491 {
492 traversalValues[out->nNodes] = next_cube_box;
493 nodeNumbers[out->nNodes] = i;
494 out->nNodes++;
495 }
496 else
497 {
498 /*
499 * If this node is not selected, we don't need to keep the next
500 * traversal value in the memory context.
501 */
502 pfree(next_cube_box);
503 }
504 }
505
506 /* Pass to the next level only the values that need to be traversed */
507 out->nodeNumbers = (int *)palloc(sizeof(int) * out->nNodes);
508 out->traversalValues = (void **)palloc(sizeof(void *) * out->nNodes);
509 for (i = 0; i < out->nNodes; i++)
510 {
511 out->nodeNumbers[i] = nodeNumbers[i];
512 out->traversalValues[i] = traversalValues[i];
513 }
514 pfree(nodeNumbers);
515 pfree(traversalValues);
516
517 /* Switch after */
518 MemoryContextSwitchTo(old_ctx);
519
520 PG_RETURN_VOID();
521}
522
523/*
524 * SP-GiST inner consistent function
525 */
527
528PGDLLEXPORT Datum gserialized_spgist_leaf_consistent_nd(PG_FUNCTION_ARGS)
529{
530 spgLeafConsistentIn *in = (spgLeafConsistentIn *)PG_GETARG_POINTER(0);
531 spgLeafConsistentOut *out = (spgLeafConsistentOut *)PG_GETARG_POINTER(1);
532 bool flag = true;
533 int i;
534 char gidxmem[GIDX_MAX_SIZE];
535 GIDX *leaf = (GIDX *)DatumGetPointer(in->leafDatum), *query_gbox_index = (GIDX *)gidxmem;
536
537 POSTGIS_DEBUG(4, "[SPGIST] 'leaf consistent' function called");
538
539 /* All tests are exact. */
540 out->recheck = false;
541
542 /* leafDatum is what it is... */
543 out->leafValue = in->leafDatum;
544
545 /* Perform the required comparison(s) */
546 for (i = 0; i < in->nkeys; i++)
547 {
548 StrategyNumber strategy = in->scankeys[i].sk_strategy;
549 Datum query = in->scankeys[i].sk_argument;
550
551 /* Quick sanity check on query argument. */
552 if (DatumGetPointer(query) == NULL)
553 {
554 POSTGIS_DEBUG(4, "[SPGIST] null query pointer (!?!)");
555 flag = false;
556 }
557
558 /* Null box should never make this far. */
559 if (gserialized_datum_get_gidx_p(query, query_gbox_index) == LW_FAILURE)
560 {
561 POSTGIS_DEBUG(4, "[SPGIST] null query_gbox_index!");
562 flag = false;
563 }
564
565 switch (strategy)
566 {
568 flag = gidx_overlaps(leaf, query_gbox_index);
569 break;
570
572 flag = gidx_contains(leaf, query_gbox_index);
573 break;
574
576 flag = gidx_contains(query_gbox_index, leaf);
577 break;
578
580 flag = gidx_equals(leaf, query_gbox_index);
581 break;
582
583 default:
584 elog(ERROR, "unrecognized strategy: %d", strategy);
585 }
586
587 /* If any check is failed, we have found our answer. */
588 if (!flag)
589 break;
590 }
591
592 PG_RETURN_BOOL(flag);
593}
594
596
597PGDLLEXPORT Datum gserialized_spgist_compress_nd(PG_FUNCTION_ARGS)
598{
599 char gidxmem[GIDX_MAX_SIZE];
600 GIDX *result = (GIDX *)gidxmem;
601 long unsigned int i;
602
603 POSTGIS_DEBUG(4, "[SPGIST] 'compress' function called");
604
605 /* Input entry is null? Return NULL. */
606 if (PG_ARGISNULL(0))
607 {
608 POSTGIS_DEBUG(4, "[SPGIST] null entry (!?!)");
609 PG_RETURN_NULL();
610 }
611
612 /* Is the bounding box valid (non-empty, non-infinite) ?
613 * If not, return NULL. */
614 if (gserialized_datum_get_gidx_p(PG_GETARG_DATUM(0), result) == LW_FAILURE)
615 {
616 POSTGIS_DEBUG(4, "[SPGIST] empty geometry!");
617 PG_RETURN_NULL();
618 }
619
620 POSTGIS_DEBUGF(4, "[SPGIST] got GIDX: %s", gidx_to_string(result));
621
622 /* Check all the dimensions for finite values.
623 * If not, use the "unknown" GIDX as a key */
624 for (i = 0; i < GIDX_NDIMS(result); i++)
625 {
626 if (!isfinite(GIDX_GET_MAX(result, i)) || !isfinite(GIDX_GET_MIN(result, i)))
627 {
629 PG_RETURN_POINTER(result);
630 }
631 }
632
633 /* Ensure bounding box has minimums below maximums. */
635
636 /* Return GIDX. */
637 PG_RETURN_POINTER(gidx_copy(result));
638}
639
640/*****************************************************************************/
char result[OUT_DOUBLE_BUFFER_SIZE]
Definition cu_print.c:267
void gidx_set_unknown(GIDX *a)
bool gidx_contains(GIDX *a, GIDX *b)
bool gidx_equals(GIDX *a, GIDX *b)
GIDX * gidx_copy(GIDX *b)
void gidx_validate(GIDX *b)
bool gidx_overlaps(GIDX *a, GIDX *b)
static CubeBox3D * initCubeBox(void)
#define SPGOverlapStrategyNumber
#define SPGSameStrategyNumber
#define SPGContainedByStrategyNumber
#define SPGContainsStrategyNumber
static bool overlapND(CubeGIDX *cube_box, GIDX *query)
static uint16_t getOctant(const GIDX *centroid, const GIDX *inBox)
Datum gserialized_spgist_compress_nd(PG_FUNCTION_ARGS)
PG_FUNCTION_INFO_V1(gserialized_spgist_config_nd)
Datum gserialized_spgist_inner_consistent_nd(PG_FUNCTION_ARGS)
Datum gserialized_spgist_config_nd(PG_FUNCTION_ARGS)
Datum gserialized_spgist_choose_nd(PG_FUNCTION_ARGS)
Datum gserialized_spgist_picksplit_nd(PG_FUNCTION_ARGS)
static CubeGIDX * nextCubeBox(CubeGIDX *cube_box, GIDX *centroid, uint16_t octant)
static bool containND(CubeGIDX *cube_box, GIDX *query)
static int compareFloats(const void *a, const void *b)
Datum gserialized_spgist_leaf_consistent_nd(PG_FUNCTION_ARGS)
#define LW_FAILURE
Definition liblwgeom.h:96
Datum centroid(PG_FUNCTION_ARGS)