Browse Source

Java port of H3 hexagonal grid library (#81714)

Adds a new lib project that contains the port to Java of basic functionality of the bar H3 library.
Ignacio Vera 3 years ago
parent
commit
93d041d837
77 changed files with 5050 additions and 0 deletions
  1. 204 0
      libs/h3/LICENSE.txt
  2. 21 0
      libs/h3/NOTICE.txt
  3. 39 0
      libs/h3/build.gradle
  4. 652 0
      libs/h3/src/main/java/org/elasticsearch/h3/BaseCells.java
  5. 57 0
      libs/h3/src/main/java/org/elasticsearch/h3/CellBoundary.java
  6. 72 0
      libs/h3/src/main/java/org/elasticsearch/h3/Constants.java
  7. 394 0
      libs/h3/src/main/java/org/elasticsearch/h3/CoordIJK.java
  8. 813 0
      libs/h3/src/main/java/org/elasticsearch/h3/FaceIJK.java
  9. 306 0
      libs/h3/src/main/java/org/elasticsearch/h3/H3.java
  10. 333 0
      libs/h3/src/main/java/org/elasticsearch/h3/H3Index.java
  11. 756 0
      libs/h3/src/main/java/org/elasticsearch/h3/HexRing.java
  12. 306 0
      libs/h3/src/main/java/org/elasticsearch/h3/Iterator.java
  13. 121 0
      libs/h3/src/main/java/org/elasticsearch/h3/LatLng.java
  14. 404 0
      libs/h3/src/main/java/org/elasticsearch/h3/Vec2d.java
  15. 83 0
      libs/h3/src/main/java/org/elasticsearch/h3/Vec3d.java
  16. 178 0
      libs/h3/src/test/java/org/elasticsearch/h3/CellBoundaryTests.java
  17. 183 0
      libs/h3/src/test/java/org/elasticsearch/h3/CellCenterTests.java
  18. 56 0
      libs/h3/src/test/java/org/elasticsearch/h3/GeoToH3Tests.java
  19. 68 0
      libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java
  20. 4 0
      libs/h3/src/test/resources/org/elasticsearch/h3/NOTICE.txt
  21. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r08cells.txt.gz
  22. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r08centers.txt.gz
  23. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r09cells.txt.gz
  24. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r09centers.txt.gz
  25. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r10cells.txt.gz
  26. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r10centers.txt.gz
  27. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r11cells.txt.gz
  28. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r11centers.txt.gz
  29. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r12cells.txt.gz
  30. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r12centers.txt.gz
  31. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r13cells.txt.gz
  32. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r13centers.txt.gz
  33. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r14cells.txt.gz
  34. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r14centers.txt.gz
  35. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r15cells.txt.gz
  36. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc05r15centers.txt.gz
  37. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r08cells.txt.gz
  38. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r08centers.txt.gz
  39. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r09cells.txt.gz
  40. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r09centers.txt.gz
  41. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r10cells.txt.gz
  42. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r10centers.txt.gz
  43. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r11cells.txt.gz
  44. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r11centers.txt.gz
  45. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r12cells.txt.gz
  46. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r12centers.txt.gz
  47. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r13cells.txt.gz
  48. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r13centers.txt.gz
  49. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r14cells.txt.gz
  50. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r14centers.txt.gz
  51. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r15cells.txt.gz
  52. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc14r15centers.txt.gz
  53. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r08cells.txt.gz
  54. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r08centers.txt.gz
  55. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r09cells.txt.gz
  56. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r09centers.txt.gz
  57. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r10cells.txt.gz
  58. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r10centers.txt.gz
  59. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r11cells.txt.gz
  60. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r11centers.txt.gz
  61. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r12cells.txt.gz
  62. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r12centers.txt.gz
  63. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r13cells.txt.gz
  64. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r13centers.txt.gz
  65. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r14cells.txt.gz
  66. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r14centers.txt.gz
  67. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r15cells.txt.gz
  68. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/bc19r15centers.txt.gz
  69. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/res00cells.txt.gz
  70. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/res00ic.txt.gz
  71. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/res01cells.txt.gz
  72. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/res01ic.txt.gz
  73. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/res02cells.txt.gz
  74. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/res02ic.txt.gz
  75. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/res03cells.txt.gz
  76. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/res03ic.txt.gz
  77. BIN
      libs/h3/src/test/resources/org/elasticsearch/h3/res04ic.txt.gz

+ 204 - 0
libs/h3/LICENSE.txt

@@ -0,0 +1,204 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.

+ 21 - 0
libs/h3/NOTICE.txt

@@ -0,0 +1,21 @@
+Elastic-hex
+
+Copyright 2022 Elasticsearch B.V.
+
+--
+
+This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.
+
+Copyright 2017-2021 Uber Technologies, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.

+ 39 - 0
libs/h3/build.gradle

@@ -0,0 +1,39 @@
+import org.elasticsearch.gradle.internal.conventions.precommit.LicenseHeadersTask
+
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+apply plugin: 'elasticsearch.build'
+apply plugin: 'elasticsearch.publish'
+
+dependencies {
+  testImplementation(project(":test:framework")) {
+    exclude group: 'org.elasticsearch', module: 'elasticsearch-geo'
+  }
+}
+
+tasks.named('forbiddenApisMain').configure {
+  // geo does not depend on server
+  // TODO: Need to decide how we want to handle for forbidden signatures with the changes to core
+  replaceSignatureFiles 'jdk-signatures'
+}
+
+tasks.withType(LicenseHeadersTask.class).configureEach {
+  approvedLicenses = ['Apache']
+}
+

+ 652 - 0
libs/h3/src/main/java/org/elasticsearch/h3/BaseCells.java

@@ -0,0 +1,652 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.
+ *
+ * Copyright 2016-2018 Uber Technologies, Inc.
+ */
+package org.elasticsearch.h3;
+
+/**
+ *  Base cell related lookup tables and access functions.
+ */
+final class BaseCells {
+
+    private static class BaseCellData {
+        // "home" face and normalized ijk coordinates on that face
+        final int homeFace;
+        final int homeI;
+        final int homeJ;
+        final int homeK;
+        // is this base cell a pentagon?
+        final boolean isPentagon;
+        // if a pentagon, what are its two clockwise offset
+        final int[] cwOffsetPent;
+
+        /// faces?
+        BaseCellData(int homeFace, int homeI, int homeJ, int homeK, boolean isPentagon, int[] cwOffsetPent) {
+            this.homeFace = homeFace;
+            this.homeI = homeI;
+            this.homeJ = homeJ;
+            this.homeK = homeK;
+            this.isPentagon = isPentagon;
+            this.cwOffsetPent = cwOffsetPent;
+        }
+    }
+
+    /**
+     * Resolution 0 base cell data table.
+     * <p>
+     * For each base cell, gives the "home" face and ijk+ coordinates on that face,
+     * whether or not the base cell is a pentagon. Additionally, if the base cell
+     * is a pentagon, the two cw offset rotation adjacent faces are given (-1
+     * indicates that no cw offset rotation faces exist for this base cell).
+     */
+    private static final BaseCellData[] baseCellData = new BaseCellData[] {
+        new BaseCellData(1, 1, 0, 0, false, new int[] { 0, 0 }),     // base cell 0
+        new BaseCellData(2, 1, 1, 0, false, new int[] { 0, 0 }),     // base cell 1
+        new BaseCellData(1, 0, 0, 0, false, new int[] { 0, 0 }),     // base cell 2
+        new BaseCellData(2, 1, 0, 0, false, new int[] { 0, 0 }),     // base cell 3
+        new BaseCellData(0, 2, 0, 0, true, new int[] { -1, -1 }),   // base cell 4
+        new BaseCellData(1, 1, 1, 0, false, new int[] { 0, 0 }),     // base cell 5
+        new BaseCellData(1, 0, 0, 1, false, new int[] { 0, 0 }),     // base cell 6
+        new BaseCellData(2, 0, 0, 0, false, new int[] { 0, 0 }),     // base cell 7
+        new BaseCellData(0, 1, 0, 0, false, new int[] { 0, 0 }),     // base cell 8
+        new BaseCellData(2, 0, 1, 0, false, new int[] { 0, 0 }),     // base cell 9
+        new BaseCellData(1, 0, 1, 0, false, new int[] { 0, 0 }),     // base cell 10
+        new BaseCellData(1, 0, 1, 1, false, new int[] { 0, 0 }),     // base cell 11
+        new BaseCellData(3, 1, 0, 0, false, new int[] { 0, 0 }),     // base cell 12
+        new BaseCellData(3, 1, 1, 0, false, new int[] { 0, 0 }),     // base cell 13
+        new BaseCellData(11, 2, 0, 0, true, new int[] { 2, 6 }),    // base cell 14
+        new BaseCellData(4, 1, 0, 0, false, new int[] { 0, 0 }),     // base cell 15
+        new BaseCellData(0, 0, 0, 0, false, new int[] { 0, 0 }),     // base cell 16
+        new BaseCellData(6, 0, 1, 0, false, new int[] { 0, 0 }),     // base cell 17
+        new BaseCellData(0, 0, 0, 1, false, new int[] { 0, 0 }),     // base cell 18
+        new BaseCellData(2, 0, 1, 1, false, new int[] { 0, 0 }),     // base cell 19
+        new BaseCellData(7, 0, 0, 1, false, new int[] { 0, 0 }),     // base cell 20
+        new BaseCellData(2, 0, 0, 1, false, new int[] { 0, 0 }),     // base cell 21
+        new BaseCellData(0, 1, 1, 0, false, new int[] { 0, 0 }),     // base cell 22
+        new BaseCellData(6, 0, 0, 1, false, new int[] { 0, 0 }),     // base cell 23
+        new BaseCellData(10, 2, 0, 0, true, new int[] { 1, 5 }),    // base cell 24
+        new BaseCellData(6, 0, 0, 0, false, new int[] { 0, 0 }),     // base cell 25
+        new BaseCellData(3, 0, 0, 0, false, new int[] { 0, 0 }),     // base cell 26
+        new BaseCellData(11, 1, 0, 0, false, new int[] { 0, 0 }),    // base cell 27
+        new BaseCellData(4, 1, 1, 0, false, new int[] { 0, 0 }),     // base cell 28
+        new BaseCellData(3, 0, 1, 0, false, new int[] { 0, 0 }),     // base cell 29
+        new BaseCellData(0, 0, 1, 1, false, new int[] { 0, 0 }),     // base cell 30
+        new BaseCellData(4, 0, 0, 0, false, new int[] { 0, 0 }),     // base cell 31
+        new BaseCellData(5, 0, 1, 0, false, new int[] { 0, 0 }),     // base cell 32
+        new BaseCellData(0, 0, 1, 0, false, new int[] { 0, 0 }),     // base cell 33
+        new BaseCellData(7, 0, 1, 0, false, new int[] { 0, 0 }),     // base cell 34
+        new BaseCellData(11, 1, 1, 0, false, new int[] { 0, 0 }),    // base cell 35
+        new BaseCellData(7, 0, 0, 0, false, new int[] { 0, 0 }),     // base cell 36
+        new BaseCellData(10, 1, 0, 0, false, new int[] { 0, 0 }),    // base cell 37
+        new BaseCellData(12, 2, 0, 0, true, new int[] { 3, 7 }),    // base cell 38
+        new BaseCellData(6, 1, 0, 1, false, new int[] { 0, 0 }),     // base cell 39
+        new BaseCellData(7, 1, 0, 1, false, new int[] { 0, 0 }),     // base cell 40
+        new BaseCellData(4, 0, 0, 1, false, new int[] { 0, 0 }),     // base cell 41
+        new BaseCellData(3, 0, 0, 1, false, new int[] { 0, 0 }),     // base cell 42
+        new BaseCellData(3, 0, 1, 1, false, new int[] { 0, 0 }),     // base cell 43
+        new BaseCellData(4, 0, 1, 0, false, new int[] { 0, 0 }),     // base cell 44
+        new BaseCellData(6, 1, 0, 0, false, new int[] { 0, 0 }),     // base cell 45
+        new BaseCellData(11, 0, 0, 0, false, new int[] { 0, 0 }),    // base cell 46
+        new BaseCellData(8, 0, 0, 1, false, new int[] { 0, 0 }),     // base cell 47
+        new BaseCellData(5, 0, 0, 1, false, new int[] { 0, 0 }),     // base cell 48
+        new BaseCellData(14, 2, 0, 0, true, new int[] { 0, 9 }),    // base cell 49
+        new BaseCellData(5, 0, 0, 0, false, new int[] { 0, 0 }),     // base cell 50
+        new BaseCellData(12, 1, 0, 0, false, new int[] { 0, 0 }),    // base cell 51
+        new BaseCellData(10, 1, 1, 0, false, new int[] { 0, 0 }),    // base cell 52
+        new BaseCellData(4, 0, 1, 1, false, new int[] { 0, 0 }),     // base cell 53
+        new BaseCellData(12, 1, 1, 0, false, new int[] { 0, 0 }),    // base cell 54
+        new BaseCellData(7, 1, 0, 0, false, new int[] { 0, 0 }),     // base cell 55
+        new BaseCellData(11, 0, 1, 0, false, new int[] { 0, 0 }),    // base cell 56
+        new BaseCellData(10, 0, 0, 0, false, new int[] { 0, 0 }),    // base cell 57
+        new BaseCellData(13, 2, 0, 0, true, new int[] { 4, 8 }),    // base cell 58
+        new BaseCellData(10, 0, 0, 1, false, new int[] { 0, 0 }),    // base cell 59
+        new BaseCellData(11, 0, 0, 1, false, new int[] { 0, 0 }),    // base cell 60
+        new BaseCellData(9, 0, 1, 0, false, new int[] { 0, 0 }),     // base cell 61
+        new BaseCellData(8, 0, 1, 0, false, new int[] { 0, 0 }),     // base cell 62
+        new BaseCellData(6, 2, 0, 0, true, new int[] { 11, 15 }),   // base cell 63
+        new BaseCellData(8, 0, 0, 0, false, new int[] { 0, 0 }),     // base cell 64
+        new BaseCellData(9, 0, 0, 1, false, new int[] { 0, 0 }),     // base cell 65
+        new BaseCellData(14, 1, 0, 0, false, new int[] { 0, 0 }),    // base cell 66
+        new BaseCellData(5, 1, 0, 1, false, new int[] { 0, 0 }),     // base cell 67
+        new BaseCellData(16, 0, 1, 1, false, new int[] { 0, 0 }),    // base cell 68
+        new BaseCellData(8, 1, 0, 1, false, new int[] { 0, 0 }),     // base cell 69
+        new BaseCellData(5, 1, 0, 0, false, new int[] { 0, 0 }),     // base cell 70
+        new BaseCellData(12, 0, 0, 0, false, new int[] { 0, 0 }),    // base cell 71
+        new BaseCellData(7, 2, 0, 0, true, new int[] { 12, 16 }),   // base cell 72
+        new BaseCellData(12, 0, 1, 0, false, new int[] { 0, 0 }),    // base cell 73
+        new BaseCellData(10, 0, 1, 0, false, new int[] { 0, 0 }),    // base cell 74
+        new BaseCellData(9, 0, 0, 0, false, new int[] { 0, 0 }),     // base cell 75
+        new BaseCellData(13, 1, 0, 0, false, new int[] { 0, 0 }),    // base cell 76
+        new BaseCellData(16, 0, 0, 1, false, new int[] { 0, 0 }),    // base cell 77
+        new BaseCellData(15, 0, 1, 1, false, new int[] { 0, 0 }),    // base cell 78
+        new BaseCellData(15, 0, 1, 0, false, new int[] { 0, 0 }),    // base cell 79
+        new BaseCellData(16, 0, 1, 0, false, new int[] { 0, 0 }),    // base cell 80
+        new BaseCellData(14, 1, 1, 0, false, new int[] { 0, 0 }),    // base cell 81
+        new BaseCellData(13, 1, 1, 0, false, new int[] { 0, 0 }),    // base cell 82
+        new BaseCellData(5, 2, 0, 0, true, new int[] { 10, 19 }),   // base cell 83
+        new BaseCellData(8, 1, 0, 0, false, new int[] { 0, 0 }),     // base cell 84
+        new BaseCellData(14, 0, 0, 0, false, new int[] { 0, 0 }),    // base cell 85
+        new BaseCellData(9, 1, 0, 1, false, new int[] { 0, 0 }),     // base cell 86
+        new BaseCellData(14, 0, 0, 1, false, new int[] { 0, 0 }),    // base cell 87
+        new BaseCellData(17, 0, 0, 1, false, new int[] { 0, 0 }),    // base cell 88
+        new BaseCellData(12, 0, 0, 1, false, new int[] { 0, 0 }),    // base cell 89
+        new BaseCellData(16, 0, 0, 0, false, new int[] { 0, 0 }),    // base cell 90
+        new BaseCellData(17, 0, 1, 1, false, new int[] { 0, 0 }),    // base cell 91
+        new BaseCellData(15, 0, 0, 1, false, new int[] { 0, 0 }),    // base cell 92
+        new BaseCellData(16, 1, 0, 1, false, new int[] { 0, 0 }),    // base cell 93
+        new BaseCellData(9, 1, 0, 0, false, new int[] { 0, 0 }),     // base cell 94
+        new BaseCellData(15, 0, 0, 0, false, new int[] { 0, 0 }),    // base cell 95
+        new BaseCellData(13, 0, 0, 0, false, new int[] { 0, 0 }),    // base cell 96
+        new BaseCellData(8, 2, 0, 0, true, new int[] { 13, 17 }),   // base cell 97
+        new BaseCellData(13, 0, 1, 0, false, new int[] { 0, 0 }),    // base cell 98
+        new BaseCellData(17, 1, 0, 1, false, new int[] { 0, 0 }),    // base cell 99
+        new BaseCellData(19, 0, 1, 0, false, new int[] { 0, 0 }),    // base cell 100
+        new BaseCellData(14, 0, 1, 0, false, new int[] { 0, 0 }),    // base cell 101
+        new BaseCellData(19, 0, 1, 1, false, new int[] { 0, 0 }),    // base cell 102
+        new BaseCellData(17, 0, 1, 0, false, new int[] { 0, 0 }),    // base cell 103
+        new BaseCellData(13, 0, 0, 1, false, new int[] { 0, 0 }),    // base cell 104
+        new BaseCellData(17, 0, 0, 0, false, new int[] { 0, 0 }),    // base cell 105
+        new BaseCellData(16, 1, 0, 0, false, new int[] { 0, 0 }),    // base cell 106
+        new BaseCellData(9, 2, 0, 0, true, new int[] { 14, 18 }),   // base cell 107
+        new BaseCellData(15, 1, 0, 1, false, new int[] { 0, 0 }),    // base cell 108
+        new BaseCellData(15, 1, 0, 0, false, new int[] { 0, 0 }),    // base cell 109
+        new BaseCellData(18, 0, 1, 1, false, new int[] { 0, 0 }),    // base cell 110
+        new BaseCellData(18, 0, 0, 1, false, new int[] { 0, 0 }),    // base cell 111
+        new BaseCellData(19, 0, 0, 1, false, new int[] { 0, 0 }),    // base cell 112
+        new BaseCellData(17, 1, 0, 0, false, new int[] { 0, 0 }),    // base cell 113
+        new BaseCellData(19, 0, 0, 0, false, new int[] { 0, 0 }),    // base cell 114
+        new BaseCellData(18, 0, 1, 0, false, new int[] { 0, 0 }),    // base cell 115
+        new BaseCellData(18, 1, 0, 1, false, new int[] { 0, 0 }),    // base cell 116
+        new BaseCellData(19, 2, 0, 0, true, new int[] { -1, -1 }),  // base cell 117
+        new BaseCellData(19, 1, 0, 0, false, new int[] { 0, 0 }),    // base cell 118
+        new BaseCellData(18, 0, 0, 0, false, new int[] { 0, 0 }),    // base cell 119
+        new BaseCellData(19, 1, 0, 1, false, new int[] { 0, 0 }),    // base cell 120
+        new BaseCellData(18, 1, 0, 0, false, new int[] { 0, 0 })     // base cell 121
+    };
+
+    /**
+     *  base cell at a given ijk and required rotations into its system
+     */
+    private static class BaseCellRotation {
+        final int baseCell;  // base cell number
+        final int ccwRot60;  // number of ccw 60 degree rotations relative to current
+        /// face
+
+        BaseCellRotation(int baseCell, int ccwRot60) {
+            this.baseCell = baseCell;
+            this.ccwRot60 = ccwRot60;
+        }
+    }
+
+    /** @brief Resolution 0 base cell lookup table for each face.
+     *
+     * Given the face number and a resolution 0 ijk+ coordinate in that face's
+     * face-centered ijk coordinate system, gives the base cell located at that
+     * coordinate and the number of 60 ccw rotations to rotate into that base
+     * cell's orientation.
+     *
+     * Valid lookup coordinates are from (0, 0, 0) to (2, 2, 2).
+     *
+     * This table can be accessed using the functions `_faceIjkToBaseCell` and
+     * `_faceIjkToBaseCellCCWrot60`
+     */
+    private static final BaseCellRotation[][][][] faceIjkBaseCells = new BaseCellRotation[][][][] {
+        {// face 0
+            {
+                // i 0
+                { new BaseCellRotation(16, 0), new BaseCellRotation(18, 0), new BaseCellRotation(24, 0) },  // j 0
+                { new BaseCellRotation(33, 0), new BaseCellRotation(30, 0), new BaseCellRotation(32, 3) },  // j 1
+                { new BaseCellRotation(49, 1), new BaseCellRotation(48, 3), new BaseCellRotation(50, 3) }   // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(8, 0), new BaseCellRotation(5, 5), new BaseCellRotation(10, 5) },    // j 0
+                { new BaseCellRotation(22, 0), new BaseCellRotation(16, 0), new BaseCellRotation(18, 0) },  // j 1
+                { new BaseCellRotation(41, 1), new BaseCellRotation(33, 0), new BaseCellRotation(30, 0) }   // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(4, 0), new BaseCellRotation(0, 5), new BaseCellRotation(2, 5) },    // j 0
+                { new BaseCellRotation(15, 1), new BaseCellRotation(8, 0), new BaseCellRotation(5, 5) },   // j 1
+                { new BaseCellRotation(31, 1), new BaseCellRotation(22, 0), new BaseCellRotation(16, 0) }  // j 2
+            } },
+        {// face 1
+            {
+                // i 0
+                { new BaseCellRotation(2, 0), new BaseCellRotation(6, 0), new BaseCellRotation(14, 0) },    // j 0
+                { new BaseCellRotation(10, 0), new BaseCellRotation(11, 0), new BaseCellRotation(17, 3) },  // j 1
+                { new BaseCellRotation(24, 1), new BaseCellRotation(23, 3), new BaseCellRotation(25, 3) }   // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(0, 0), new BaseCellRotation(1, 5), new BaseCellRotation(9, 5) },    // j 0
+                { new BaseCellRotation(5, 0), new BaseCellRotation(2, 0), new BaseCellRotation(6, 0) },    // j 1
+                { new BaseCellRotation(18, 1), new BaseCellRotation(10, 0), new BaseCellRotation(11, 0) }  // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(4, 1), new BaseCellRotation(3, 5), new BaseCellRotation(7, 5) },  // j 0
+                { new BaseCellRotation(8, 1), new BaseCellRotation(0, 0), new BaseCellRotation(1, 5) },  // j 1
+                { new BaseCellRotation(16, 1), new BaseCellRotation(5, 0), new BaseCellRotation(2, 0) }  // j 2
+            } },
+        {// face 2
+            {
+                // i 0
+                { new BaseCellRotation(7, 0), new BaseCellRotation(21, 0), new BaseCellRotation(38, 0) },  // j 0
+                { new BaseCellRotation(9, 0), new BaseCellRotation(19, 0), new BaseCellRotation(34, 3) },  // j 1
+                { new BaseCellRotation(14, 1), new BaseCellRotation(20, 3), new BaseCellRotation(36, 3) }  // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(3, 0), new BaseCellRotation(13, 5), new BaseCellRotation(29, 5) },  // j 0
+                { new BaseCellRotation(1, 0), new BaseCellRotation(7, 0), new BaseCellRotation(21, 0) },   // j 1
+                { new BaseCellRotation(6, 1), new BaseCellRotation(9, 0), new BaseCellRotation(19, 0) }    // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(4, 2), new BaseCellRotation(12, 5), new BaseCellRotation(26, 5) },  // j 0
+                { new BaseCellRotation(0, 1), new BaseCellRotation(3, 0), new BaseCellRotation(13, 5) },   // j 1
+                { new BaseCellRotation(2, 1), new BaseCellRotation(1, 0), new BaseCellRotation(7, 0) }     // j 2
+            } },
+        {// face 3
+            {
+                // i 0
+                { new BaseCellRotation(26, 0), new BaseCellRotation(42, 0), new BaseCellRotation(58, 0) },  // j 0
+                { new BaseCellRotation(29, 0), new BaseCellRotation(43, 0), new BaseCellRotation(62, 3) },  // j 1
+                { new BaseCellRotation(38, 1), new BaseCellRotation(47, 3), new BaseCellRotation(64, 3) }   // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(12, 0), new BaseCellRotation(28, 5), new BaseCellRotation(44, 5) },  // j 0
+                { new BaseCellRotation(13, 0), new BaseCellRotation(26, 0), new BaseCellRotation(42, 0) },  // j 1
+                { new BaseCellRotation(21, 1), new BaseCellRotation(29, 0), new BaseCellRotation(43, 0) }   // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(4, 3), new BaseCellRotation(15, 5), new BaseCellRotation(31, 5) },  // j 0
+                { new BaseCellRotation(3, 1), new BaseCellRotation(12, 0), new BaseCellRotation(28, 5) },  // j 1
+                { new BaseCellRotation(7, 1), new BaseCellRotation(13, 0), new BaseCellRotation(26, 0) }   // j 2
+            } },
+        {// face 4
+            {
+                // i 0
+                { new BaseCellRotation(31, 0), new BaseCellRotation(41, 0), new BaseCellRotation(49, 0) },  // j 0
+                { new BaseCellRotation(44, 0), new BaseCellRotation(53, 0), new BaseCellRotation(61, 3) },  // j 1
+                { new BaseCellRotation(58, 1), new BaseCellRotation(65, 3), new BaseCellRotation(75, 3) }   // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(15, 0), new BaseCellRotation(22, 5), new BaseCellRotation(33, 5) },  // j 0
+                { new BaseCellRotation(28, 0), new BaseCellRotation(31, 0), new BaseCellRotation(41, 0) },  // j 1
+                { new BaseCellRotation(42, 1), new BaseCellRotation(44, 0), new BaseCellRotation(53, 0) }   // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(4, 4), new BaseCellRotation(8, 5), new BaseCellRotation(16, 5) },    // j 0
+                { new BaseCellRotation(12, 1), new BaseCellRotation(15, 0), new BaseCellRotation(22, 5) },  // j 1
+                { new BaseCellRotation(26, 1), new BaseCellRotation(28, 0), new BaseCellRotation(31, 0) }   // j 2
+            } },
+        {// face 5
+            {
+                // i 0
+                { new BaseCellRotation(50, 0), new BaseCellRotation(48, 0), new BaseCellRotation(49, 3) },  // j 0
+                { new BaseCellRotation(32, 0), new BaseCellRotation(30, 3), new BaseCellRotation(33, 3) },  // j 1
+                { new BaseCellRotation(24, 3), new BaseCellRotation(18, 3), new BaseCellRotation(16, 3) }   // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(70, 0), new BaseCellRotation(67, 0), new BaseCellRotation(66, 3) },  // j 0
+                { new BaseCellRotation(52, 3), new BaseCellRotation(50, 0), new BaseCellRotation(48, 0) },  // j 1
+                { new BaseCellRotation(37, 3), new BaseCellRotation(32, 0), new BaseCellRotation(30, 3) }   // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(83, 0), new BaseCellRotation(87, 3), new BaseCellRotation(85, 3) },  // j 0
+                { new BaseCellRotation(74, 3), new BaseCellRotation(70, 0), new BaseCellRotation(67, 0) },  // j 1
+                { new BaseCellRotation(57, 1), new BaseCellRotation(52, 3), new BaseCellRotation(50, 0) }   // j 2
+            } },
+        {// face 6
+            {
+                // i 0
+                { new BaseCellRotation(25, 0), new BaseCellRotation(23, 0), new BaseCellRotation(24, 3) },  // j 0
+                { new BaseCellRotation(17, 0), new BaseCellRotation(11, 3), new BaseCellRotation(10, 3) },  // j 1
+                { new BaseCellRotation(14, 3), new BaseCellRotation(6, 3), new BaseCellRotation(2, 3) }     // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(45, 0), new BaseCellRotation(39, 0), new BaseCellRotation(37, 3) },  // j 0
+                { new BaseCellRotation(35, 3), new BaseCellRotation(25, 0), new BaseCellRotation(23, 0) },  // j 1
+                { new BaseCellRotation(27, 3), new BaseCellRotation(17, 0), new BaseCellRotation(11, 3) }   // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(63, 0), new BaseCellRotation(59, 3), new BaseCellRotation(57, 3) },  // j 0
+                { new BaseCellRotation(56, 3), new BaseCellRotation(45, 0), new BaseCellRotation(39, 0) },  // j 1
+                { new BaseCellRotation(46, 3), new BaseCellRotation(35, 3), new BaseCellRotation(25, 0) }   // j 2
+            } },
+        {// face 7
+            {
+                // i 0
+                { new BaseCellRotation(36, 0), new BaseCellRotation(20, 0), new BaseCellRotation(14, 3) },  // j 0
+                { new BaseCellRotation(34, 0), new BaseCellRotation(19, 3), new BaseCellRotation(9, 3) },   // j 1
+                { new BaseCellRotation(38, 3), new BaseCellRotation(21, 3), new BaseCellRotation(7, 3) }    // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(55, 0), new BaseCellRotation(40, 0), new BaseCellRotation(27, 3) },  // j 0
+                { new BaseCellRotation(54, 3), new BaseCellRotation(36, 0), new BaseCellRotation(20, 0) },  // j 1
+                { new BaseCellRotation(51, 3), new BaseCellRotation(34, 0), new BaseCellRotation(19, 3) }   // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(72, 0), new BaseCellRotation(60, 3), new BaseCellRotation(46, 3) },  // j 0
+                { new BaseCellRotation(73, 3), new BaseCellRotation(55, 0), new BaseCellRotation(40, 0) },  // j 1
+                { new BaseCellRotation(71, 3), new BaseCellRotation(54, 3), new BaseCellRotation(36, 0) }   // j 2
+            } },
+        {// face 8
+            {
+                // i 0
+                { new BaseCellRotation(64, 0), new BaseCellRotation(47, 0), new BaseCellRotation(38, 3) },  // j 0
+                { new BaseCellRotation(62, 0), new BaseCellRotation(43, 3), new BaseCellRotation(29, 3) },  // j 1
+                { new BaseCellRotation(58, 3), new BaseCellRotation(42, 3), new BaseCellRotation(26, 3) }   // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(84, 0), new BaseCellRotation(69, 0), new BaseCellRotation(51, 3) },  // j 0
+                { new BaseCellRotation(82, 3), new BaseCellRotation(64, 0), new BaseCellRotation(47, 0) },  // j 1
+                { new BaseCellRotation(76, 3), new BaseCellRotation(62, 0), new BaseCellRotation(43, 3) }   // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(97, 0), new BaseCellRotation(89, 3), new BaseCellRotation(71, 3) },  // j 0
+                { new BaseCellRotation(98, 3), new BaseCellRotation(84, 0), new BaseCellRotation(69, 0) },  // j 1
+                { new BaseCellRotation(96, 3), new BaseCellRotation(82, 3), new BaseCellRotation(64, 0) }   // j 2
+            } },
+        {// face 9
+            {
+                // i 0
+                { new BaseCellRotation(75, 0), new BaseCellRotation(65, 0), new BaseCellRotation(58, 3) },  // j 0
+                { new BaseCellRotation(61, 0), new BaseCellRotation(53, 3), new BaseCellRotation(44, 3) },  // j 1
+                { new BaseCellRotation(49, 3), new BaseCellRotation(41, 3), new BaseCellRotation(31, 3) }   // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(94, 0), new BaseCellRotation(86, 0), new BaseCellRotation(76, 3) },  // j 0
+                { new BaseCellRotation(81, 3), new BaseCellRotation(75, 0), new BaseCellRotation(65, 0) },  // j 1
+                { new BaseCellRotation(66, 3), new BaseCellRotation(61, 0), new BaseCellRotation(53, 3) }   // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(107, 0), new BaseCellRotation(104, 3), new BaseCellRotation(96, 3) },  // j 0
+                { new BaseCellRotation(101, 3), new BaseCellRotation(94, 0), new BaseCellRotation(86, 0) },   // j 1
+                { new BaseCellRotation(85, 3), new BaseCellRotation(81, 3), new BaseCellRotation(75, 0) }     // j 2
+            } },
+        {// face 10
+            {
+                // i 0
+                { new BaseCellRotation(57, 0), new BaseCellRotation(59, 0), new BaseCellRotation(63, 3) },  // j 0
+                { new BaseCellRotation(74, 0), new BaseCellRotation(78, 3), new BaseCellRotation(79, 3) },  // j 1
+                { new BaseCellRotation(83, 3), new BaseCellRotation(92, 3), new BaseCellRotation(95, 3) }   // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(37, 0), new BaseCellRotation(39, 3), new BaseCellRotation(45, 3) },  // j 0
+                { new BaseCellRotation(52, 0), new BaseCellRotation(57, 0), new BaseCellRotation(59, 0) },  // j 1
+                { new BaseCellRotation(70, 3), new BaseCellRotation(74, 0), new BaseCellRotation(78, 3) }   // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(24, 0), new BaseCellRotation(23, 3), new BaseCellRotation(25, 3) },  // j 0
+                { new BaseCellRotation(32, 3), new BaseCellRotation(37, 0), new BaseCellRotation(39, 3) },  // j 1
+                { new BaseCellRotation(50, 3), new BaseCellRotation(52, 0), new BaseCellRotation(57, 0) }   // j 2
+            } },
+        {// face 11
+            {
+                // i 0
+                { new BaseCellRotation(46, 0), new BaseCellRotation(60, 0), new BaseCellRotation(72, 3) },  // j 0
+                { new BaseCellRotation(56, 0), new BaseCellRotation(68, 3), new BaseCellRotation(80, 3) },  // j 1
+                { new BaseCellRotation(63, 3), new BaseCellRotation(77, 3), new BaseCellRotation(90, 3) }   // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(27, 0), new BaseCellRotation(40, 3), new BaseCellRotation(55, 3) },  // j 0
+                { new BaseCellRotation(35, 0), new BaseCellRotation(46, 0), new BaseCellRotation(60, 0) },  // j 1
+                { new BaseCellRotation(45, 3), new BaseCellRotation(56, 0), new BaseCellRotation(68, 3) }   // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(14, 0), new BaseCellRotation(20, 3), new BaseCellRotation(36, 3) },  // j 0
+                { new BaseCellRotation(17, 3), new BaseCellRotation(27, 0), new BaseCellRotation(40, 3) },  // j 1
+                { new BaseCellRotation(25, 3), new BaseCellRotation(35, 0), new BaseCellRotation(46, 0) }   // j 2
+            } },
+        {// face 12
+            {
+                // i 0
+                { new BaseCellRotation(71, 0), new BaseCellRotation(89, 0), new BaseCellRotation(97, 3) },   // j 0
+                { new BaseCellRotation(73, 0), new BaseCellRotation(91, 3), new BaseCellRotation(103, 3) },  // j 1
+                { new BaseCellRotation(72, 3), new BaseCellRotation(88, 3), new BaseCellRotation(105, 3) }   // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(51, 0), new BaseCellRotation(69, 3), new BaseCellRotation(84, 3) },  // j 0
+                { new BaseCellRotation(54, 0), new BaseCellRotation(71, 0), new BaseCellRotation(89, 0) },  // j 1
+                { new BaseCellRotation(55, 3), new BaseCellRotation(73, 0), new BaseCellRotation(91, 3) }   // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(38, 0), new BaseCellRotation(47, 3), new BaseCellRotation(64, 3) },  // j 0
+                { new BaseCellRotation(34, 3), new BaseCellRotation(51, 0), new BaseCellRotation(69, 3) },  // j 1
+                { new BaseCellRotation(36, 3), new BaseCellRotation(54, 0), new BaseCellRotation(71, 0) }   // j 2
+            } },
+        {// face 13
+            {
+                // i 0
+                { new BaseCellRotation(96, 0), new BaseCellRotation(104, 0), new BaseCellRotation(107, 3) },  // j 0
+                { new BaseCellRotation(98, 0), new BaseCellRotation(110, 3), new BaseCellRotation(115, 3) },  // j 1
+                { new BaseCellRotation(97, 3), new BaseCellRotation(111, 3), new BaseCellRotation(119, 3) }   // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(76, 0), new BaseCellRotation(86, 3), new BaseCellRotation(94, 3) },   // j 0
+                { new BaseCellRotation(82, 0), new BaseCellRotation(96, 0), new BaseCellRotation(104, 0) },  // j 1
+                { new BaseCellRotation(84, 3), new BaseCellRotation(98, 0), new BaseCellRotation(110, 3) }   // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(58, 0), new BaseCellRotation(65, 3), new BaseCellRotation(75, 3) },  // j 0
+                { new BaseCellRotation(62, 3), new BaseCellRotation(76, 0), new BaseCellRotation(86, 3) },  // j 1
+                { new BaseCellRotation(64, 3), new BaseCellRotation(82, 0), new BaseCellRotation(96, 0) }   // j 2
+            } },
+        {// face 14
+            {
+                // i 0
+                { new BaseCellRotation(85, 0), new BaseCellRotation(87, 0), new BaseCellRotation(83, 3) },     // j 0
+                { new BaseCellRotation(101, 0), new BaseCellRotation(102, 3), new BaseCellRotation(100, 3) },  // j 1
+                { new BaseCellRotation(107, 3), new BaseCellRotation(112, 3), new BaseCellRotation(114, 3) }   // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(66, 0), new BaseCellRotation(67, 3), new BaseCellRotation(70, 3) },   // j 0
+                { new BaseCellRotation(81, 0), new BaseCellRotation(85, 0), new BaseCellRotation(87, 0) },   // j 1
+                { new BaseCellRotation(94, 3), new BaseCellRotation(101, 0), new BaseCellRotation(102, 3) }  // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(49, 0), new BaseCellRotation(48, 3), new BaseCellRotation(50, 3) },  // j 0
+                { new BaseCellRotation(61, 3), new BaseCellRotation(66, 0), new BaseCellRotation(67, 3) },  // j 1
+                { new BaseCellRotation(75, 3), new BaseCellRotation(81, 0), new BaseCellRotation(85, 0) }   // j 2
+            } },
+        {// face 15
+            {
+                // i 0
+                { new BaseCellRotation(95, 0), new BaseCellRotation(92, 0), new BaseCellRotation(83, 0) },  // j 0
+                { new BaseCellRotation(79, 0), new BaseCellRotation(78, 0), new BaseCellRotation(74, 3) },  // j 1
+                { new BaseCellRotation(63, 1), new BaseCellRotation(59, 3), new BaseCellRotation(57, 3) }   // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(109, 0), new BaseCellRotation(108, 0), new BaseCellRotation(100, 5) },  // j 0
+                { new BaseCellRotation(93, 1), new BaseCellRotation(95, 0), new BaseCellRotation(92, 0) },     // j 1
+                { new BaseCellRotation(77, 1), new BaseCellRotation(79, 0), new BaseCellRotation(78, 0) }      // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(117, 4), new BaseCellRotation(118, 5), new BaseCellRotation(114, 5) },  // j 0
+                { new BaseCellRotation(106, 1), new BaseCellRotation(109, 0), new BaseCellRotation(108, 0) },  // j 1
+                { new BaseCellRotation(90, 1), new BaseCellRotation(93, 1), new BaseCellRotation(95, 0) }      // j 2
+            } },
+        {// face 16
+            {
+                // i 0
+                { new BaseCellRotation(90, 0), new BaseCellRotation(77, 0), new BaseCellRotation(63, 0) },  // j 0
+                { new BaseCellRotation(80, 0), new BaseCellRotation(68, 0), new BaseCellRotation(56, 3) },  // j 1
+                { new BaseCellRotation(72, 1), new BaseCellRotation(60, 3), new BaseCellRotation(46, 3) }   // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(106, 0), new BaseCellRotation(93, 0), new BaseCellRotation(79, 5) },  // j 0
+                { new BaseCellRotation(99, 1), new BaseCellRotation(90, 0), new BaseCellRotation(77, 0) },   // j 1
+                { new BaseCellRotation(88, 1), new BaseCellRotation(80, 0), new BaseCellRotation(68, 0) }    // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(117, 3), new BaseCellRotation(109, 5), new BaseCellRotation(95, 5) },  // j 0
+                { new BaseCellRotation(113, 1), new BaseCellRotation(106, 0), new BaseCellRotation(93, 0) },  // j 1
+                { new BaseCellRotation(105, 1), new BaseCellRotation(99, 1), new BaseCellRotation(90, 0) }    // j 2
+            } },
+        {// face 17
+            {
+                // i 0
+                { new BaseCellRotation(105, 0), new BaseCellRotation(88, 0), new BaseCellRotation(72, 0) },  // j 0
+                { new BaseCellRotation(103, 0), new BaseCellRotation(91, 0), new BaseCellRotation(73, 3) },  // j 1
+                { new BaseCellRotation(97, 1), new BaseCellRotation(89, 3), new BaseCellRotation(71, 3) }    // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(113, 0), new BaseCellRotation(99, 0), new BaseCellRotation(80, 5) },   // j 0
+                { new BaseCellRotation(116, 1), new BaseCellRotation(105, 0), new BaseCellRotation(88, 0) },  // j 1
+                { new BaseCellRotation(111, 1), new BaseCellRotation(103, 0), new BaseCellRotation(91, 0) }   // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(117, 2), new BaseCellRotation(106, 5), new BaseCellRotation(90, 5) },  // j 0
+                { new BaseCellRotation(121, 1), new BaseCellRotation(113, 0), new BaseCellRotation(99, 0) },  // j 1
+                { new BaseCellRotation(119, 1), new BaseCellRotation(116, 1), new BaseCellRotation(105, 0) }  // j 2
+            } },
+        {// face 18
+            {
+                // i 0
+                { new BaseCellRotation(119, 0), new BaseCellRotation(111, 0), new BaseCellRotation(97, 0) },  // j 0
+                { new BaseCellRotation(115, 0), new BaseCellRotation(110, 0), new BaseCellRotation(98, 3) },  // j 1
+                { new BaseCellRotation(107, 1), new BaseCellRotation(104, 3), new BaseCellRotation(96, 3) }   // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(121, 0), new BaseCellRotation(116, 0), new BaseCellRotation(103, 5) },  // j 0
+                { new BaseCellRotation(120, 1), new BaseCellRotation(119, 0), new BaseCellRotation(111, 0) },  // j 1
+                { new BaseCellRotation(112, 1), new BaseCellRotation(115, 0), new BaseCellRotation(110, 0) }   // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(117, 1), new BaseCellRotation(113, 5), new BaseCellRotation(105, 5) },  // j 0
+                { new BaseCellRotation(118, 1), new BaseCellRotation(121, 0), new BaseCellRotation(116, 0) },  // j 1
+                { new BaseCellRotation(114, 1), new BaseCellRotation(120, 1), new BaseCellRotation(119, 0) }   // j 2
+            } },
+        {// face 19
+            {
+                // i 0
+                { new BaseCellRotation(114, 0), new BaseCellRotation(112, 0), new BaseCellRotation(107, 0) },  // j 0
+                { new BaseCellRotation(100, 0), new BaseCellRotation(102, 0), new BaseCellRotation(101, 3) },  // j 1
+                { new BaseCellRotation(83, 1), new BaseCellRotation(87, 3), new BaseCellRotation(85, 3) }      // j 2
+            },
+            {
+                // i 1
+                { new BaseCellRotation(118, 0), new BaseCellRotation(120, 0), new BaseCellRotation(115, 5) },  // j 0
+                { new BaseCellRotation(108, 1), new BaseCellRotation(114, 0), new BaseCellRotation(112, 0) },  // j 1
+                { new BaseCellRotation(92, 1), new BaseCellRotation(100, 0), new BaseCellRotation(102, 0) }    // j 2
+            },
+            {
+                // i 2
+                { new BaseCellRotation(117, 0), new BaseCellRotation(121, 5), new BaseCellRotation(119, 5) },  // j 0
+                { new BaseCellRotation(109, 1), new BaseCellRotation(118, 0), new BaseCellRotation(120, 0) },  // j 1
+                { new BaseCellRotation(95, 1), new BaseCellRotation(108, 1), new BaseCellRotation(114, 0) }    // j 2
+            } } };
+
+    /**
+     *  Return whether or not the indicated base cell is a pentagon.
+     */
+    public static boolean isBaseCellPentagon(int baseCell) {
+        if (baseCell < 0 || baseCell >= Constants.NUM_BASE_CELLS) {  // LCOV_EXCL_BR_LINE
+            // Base cells less than zero can not be represented in an index
+            return false;
+        }
+        return baseCellData[baseCell].isPentagon;
+    }
+
+    /**
+     *  Return whether or not the indicated base cell is a pentagon.
+     */
+    public static FaceIJK getBaseFaceIJK(int baseCell) {
+        if (baseCell < 0 || baseCell >= Constants.NUM_BASE_CELLS) {  // LCOV_EXCL_BR_LINE
+            // Base cells less than zero can not be represented in an index
+            throw new IllegalArgumentException("Illegal base cell");
+        }
+        BaseCellData cellData = baseCellData[baseCell];
+        return new FaceIJK(cellData.homeFace, new CoordIJK(cellData.homeI, cellData.homeJ, cellData.homeK));
+    }
+
+    /** Find base cell given FaceIJK.
+     *
+     * Given the face number and a resolution 0 ijk+ coordinate in that face's
+     * face-centered ijk coordinate system, return the base cell located at that
+     * coordinate.
+     *
+     * Valid ijk+ lookup coordinates are from (0, 0, 0) to (2, 2, 2).
+     */
+    public static int getBaseCell(FaceIJK faceIJK) {
+        return faceIjkBaseCells[faceIJK.face][faceIJK.coord.i][faceIJK.coord.j][faceIJK.coord.k].baseCell;
+    }
+
+    /** Find base cell given FaceIJK.
+     *
+     * Given the face number and a resolution 0 ijk+ coordinate in that face's
+     * face-centered ijk coordinate system, return the number of 60' ccw rotations
+     * to rotate into the coordinate system of the base cell at that coordinates.
+     *
+     * Valid ijk+ lookup coordinates are from (0, 0, 0) to (2, 2, 2).
+     */
+    public static int getBaseCellCCWrot60(FaceIJK faceIJK) {
+        return faceIjkBaseCells[faceIJK.face][faceIJK.coord.i][faceIJK.coord.j][faceIJK.coord.k].ccwRot60;
+    }
+
+    /**  Return whether or not the tested face is a cw offset face.
+     */
+    public static boolean baseCellIsCwOffset(int baseCell, int testFace) {
+        return baseCellData[baseCell].cwOffsetPent[0] == testFace || baseCellData[baseCell].cwOffsetPent[1] == testFace;
+    }
+
+    /** Return whether the indicated base cell is a pentagon where all
+     * neighbors are oriented towards it. */
+    public static boolean isBaseCellPolarPentagon(int baseCell) {
+        return baseCell == 4 || baseCell == 117;
+    }
+
+}

+ 57 - 0
libs/h3/src/main/java/org/elasticsearch/h3/CellBoundary.java

@@ -0,0 +1,57 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.
+ *
+ * Copyright 2016-2021 Uber Technologies, Inc.
+ */
+package org.elasticsearch.h3;
+
+/**
+ * cell boundary points as {@link LatLng}
+ */
+public final class CellBoundary {
+
+    /** Maximum number of cell boundary vertices; worst case is pentagon:
+     *  5 original verts + 5 edge crossings
+     */
+    private static final int MAX_CELL_BNDRY_VERTS = 10;
+    /** How many points it holds */
+    private int numVertext;
+    /** The actual points */
+    private final LatLng[] points = new LatLng[MAX_CELL_BNDRY_VERTS];
+
+    CellBoundary() {}
+
+    void add(LatLng point) {
+        points[numVertext++] = point;
+    }
+
+    /** Number of points in this boundary */
+    public int numPoints() {
+        return numVertext;
+    }
+
+    /** Return the point at the given position*/
+    public LatLng getLatLon(int i) {
+        if (i >= numVertext) {
+            throw new IndexOutOfBoundsException();
+        }
+        return points[i];
+    }
+}

+ 72 - 0
libs/h3/src/main/java/org/elasticsearch/h3/Constants.java

@@ -0,0 +1,72 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.
+ *
+ * Copyright 2016-2017, 2020 Uber Technologies, Inc.
+ */
+package org.elasticsearch.h3;
+
+/**
+ * Constants used by more than one source code file.
+ */
+final class Constants {
+    /**
+     * sqrt(3) / 2.0
+     */
+    public static double M_SQRT3_2 = 0.8660254037844386467637231707529361834714;
+    /**
+     * max H3 resolution; H3 version 1 has 16 resolutions, numbered 0 through 15
+     */
+    public static int MAX_H3_RES = 15;
+    /**
+     * The number of H3 base cells
+     */
+    public static int NUM_BASE_CELLS = 122;
+    /**
+     * The number of vertices in a hexagon
+     */
+    public static int NUM_HEX_VERTS = 6;
+    /**
+     * The number of vertices in a pentagon
+     */
+    public static int NUM_PENT_VERTS = 5;
+    /**
+     * H3 index modes
+     */
+    public static int H3_CELL_MODE = 1;
+    /**
+     * square root of 7
+     */
+    public static final double M_SQRT7 = 2.6457513110645905905016157536392604257102;
+    /**
+     * scaling factor from hex2d resolution 0 unit length
+     * (or distance between adjacent cell center points
+     * on the plane) to gnomonic unit length.
+     */
+    public static double RES0_U_GNOMONIC = 0.38196601125010500003;
+    /**
+     * rotation angle between Class II and Class III resolution axes
+     * (asin(sqrt(3.0 / 28.0)))
+     */
+    public static double M_AP7_ROT_RADS = 0.333473172251832115336090755351601070065900389;
+    /**
+     * threshold epsilon
+     */
+    public static double EPSILON = 0.0000000000000001;
+}

+ 394 - 0
libs/h3/src/main/java/org/elasticsearch/h3/CoordIJK.java

@@ -0,0 +1,394 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.
+ *
+ * Copyright 2016-2018, 2020-2021 Uber Technologies, Inc.
+ */
+package org.elasticsearch.h3;
+
+/**
+ * Mutable IJK hexagon coordinates
+ *
+ * Each axis is spaced 120 degrees apart.
+ *
+ * References two Vec2d cartesian coordinate systems:
+ *
+ *    1. gnomonic: face-centered polyhedral gnomonic projection space with
+ *             traditional scaling and x-axes aligned with the face Class II
+ *             i-axes.
+ *
+ *    2. hex2d: local face-centered coordinate system scaled a specific H3 grid
+ *             resolution unit length and with x-axes aligned with the local
+ *             i-axes
+ */
+final class CoordIJK {
+
+    /** CoordIJK unit vectors corresponding to the 7 H3 digits.
+     */
+    private static final int[][] UNIT_VECS = {
+        { 0, 0, 0 },  // direction 0
+        { 0, 0, 1 },  // direction 1
+        { 0, 1, 0 },  // direction 2
+        { 0, 1, 1 },  // direction 3
+        { 1, 0, 0 },  // direction 4
+        { 1, 0, 1 },  // direction 5
+        { 1, 1, 0 }   // direction 6
+    };
+
+    /** H3 digit representing ijk+ axes direction.
+     * Values will be within the lowest 3 bits of an integer.
+     */
+    public enum Direction {
+
+        CENTER_DIGIT(0),
+        K_AXES_DIGIT(1),
+        J_AXES_DIGIT(2),
+        JK_AXES_DIGIT(J_AXES_DIGIT.digit() | K_AXES_DIGIT.digit()),
+        I_AXES_DIGIT(4),
+        IK_AXES_DIGIT(I_AXES_DIGIT.digit() | K_AXES_DIGIT.digit()),
+        IJ_AXES_DIGIT(I_AXES_DIGIT.digit() | J_AXES_DIGIT.digit()),
+        INVALID_DIGIT(7),
+        NUM_DIGITS(INVALID_DIGIT.digit()),
+        PENTAGON_SKIPPED_DIGIT(K_AXES_DIGIT.digit());
+
+        Direction(int digit) {
+            this.digit = digit;
+        }
+
+        private final int digit;
+
+        public int digit() {
+            return digit;
+        }
+
+    }
+
+    int i;  // i component
+    int j;  // j component
+    int k;  // k component
+
+    CoordIJK(int i, int j, int k) {
+        this.i = i;
+        this.j = j;
+        this.k = k;
+    }
+
+    /**
+     * Find the center point in 2D cartesian coordinates of a hex.
+     *
+     */
+    public Vec2d ijkToHex2d() {
+        int i = this.i - this.k;
+        int j = this.j - this.k;
+        return new Vec2d(i - 0.5 * j, j * Constants.M_SQRT3_2);
+    }
+
+    /**
+     * Add ijk coordinates.
+     *
+     * @param i the i coordinate
+     * @param j the j coordinate
+     * @param k the k coordinate
+     */
+
+    public void ijkAdd(int i, int j, int k) {
+        this.i += i;
+        this.j += j;
+        this.k += k;
+    }
+
+    /**
+     * Subtract ijk coordinates.
+     *
+     * @param i the i coordinate
+     * @param j the j coordinate
+     * @param k the k coordinate
+     */
+    public void ijkSub(int i, int j, int k) {
+        this.i -= i;
+        this.j -= j;
+        this.k -= k;
+    }
+
+    /**
+     * Normalizes ijk coordinates by setting the ijk coordinates
+     * to the smallest possible values.
+     */
+    public void ijkNormalize() {
+        // remove any negative values
+        if (i < 0) {
+            j -= i;
+            k -= i;
+            i = 0;
+        }
+
+        if (j < 0) {
+            i -= j;
+            k -= j;
+            j = 0;
+        }
+
+        if (k < 0) {
+            i -= k;
+            j -= k;
+            k = 0;
+        }
+
+        // remove the min value if needed
+        int min = i;
+        if (j < min) {
+            min = j;
+        }
+        if (k < min) {
+            min = k;
+        }
+        if (min > 0) {
+            i -= min;
+            j -= min;
+            k -= min;
+        }
+    }
+
+    /**
+     * Find the normalized ijk coordinates of the hex centered on the current
+     * hex at the next finer aperture 7 counter-clockwise resolution.
+     */
+    public void downAp7() {
+        // res r unit vectors in res r+1
+        // iVec (3, 0, 1)
+        // jVec (1, 3, 0)
+        // kVec (0, 1, 3)
+        final int i = this.i * 3 + this.j * 1 + this.k * 0;
+        final int j = this.i * 0 + this.j * 3 + this.k * 1;
+        final int k = this.i * 1 + this.j * 0 + this.k * 3;
+        this.i = i;
+        this.j = j;
+        this.k = k;
+        ijkNormalize();
+    }
+
+    /**
+     * Find the normalized ijk coordinates of the hex centered on the current
+     * hex at the next finer aperture 7 clockwise resolution.
+     */
+    public void downAp7r() {
+        // iVec (3, 1, 0)
+        // jVec (0, 3, 1)
+        // kVec (1, 0, 3)
+        final int i = this.i * 3 + this.j * 0 + this.k * 1;
+        final int j = this.i * 1 + this.j * 3 + this.k * 0;
+        final int k = this.i * 0 + this.j * 1 + this.k * 3;
+        this.i = i;
+        this.j = j;
+        this.k = k;
+        ijkNormalize();
+    }
+
+    /**
+     * Find the normalized ijk coordinates of the hex centered on the current
+     * hex at the next finer aperture 3 counter-clockwise resolution.
+     */
+    public void downAp3() {
+        // res r unit vectors in res r+1
+        // iVec (2, 0, 1)
+        // jVec (1, 2, 0)
+        // kVec (0, 1, 2)
+        final int i = this.i * 2 + this.j * 1 + this.k * 0;
+        final int j = this.i * 0 + this.j * 2 + this.k * 1;
+        final int k = this.i * 1 + this.j * 0 + this.k * 2;
+        this.i = i;
+        this.j = j;
+        this.k = k;
+        ijkNormalize();
+    }
+
+    /**
+     * Find the normalized ijk coordinates of the hex centered on the current
+     * hex at the next finer aperture 3 clockwise resolution.
+     */
+    public void downAp3r() {
+        // res r unit vectors in res r+1
+        // iVec (2, 1, 0)
+        // jVec (0, 2, 1)
+        // kVec (1, 0, 2)
+        final int i = this.i * 2 + this.j * 0 + this.k * 1;
+        final int j = this.i * 1 + this.j * 2 + this.k * 0;
+        final int k = this.i * 0 + this.j * 1 + this.k * 2;
+        this.i = i;
+        this.j = j;
+        this.k = k;
+        ijkNormalize();
+    }
+
+    /**
+     * Rotates ijk coordinates 60 degrees clockwise.
+     *
+     */
+    public void ijkRotate60cw() {
+        // unit vector rotations
+        // iVec (1, 0, 1)
+        // jVec (1, 1, 0)
+        // kVec (0, 1, 1)
+        final int i = this.i * 1 + this.j * 1 + this.k * 0;
+        final int j = this.i * 0 + this.j * 1 + this.k * 1;
+        final int k = this.i * 1 + this.j * 0 + this.k * 1;
+        this.i = i;
+        this.j = j;
+        this.k = k;
+        ijkNormalize();
+    }
+
+    /**
+     * Rotates ijk coordinates 60 degrees counter-clockwise.
+     */
+    public void ijkRotate60ccw() {
+        // unit vector rotations
+        // iVec (1, 1, 0)
+        // jVec (0, 1, 1)
+        // kVec (1, 0, 1)
+        final int i = this.i * 1 + this.j * 0 + this.k * 1;
+        final int j = this.i * 1 + this.j * 1 + this.k * 0;
+        final int k = this.i * 0 + this.j * 1 + this.k * 1;
+        this.i = i;
+        this.j = j;
+        this.k = k;
+        ijkNormalize();
+    }
+
+    /**
+     * Find the normalized ijk coordinates of the hex in the specified digit
+     * direction from the current ijk coordinates.
+     * @param digit The digit direction from the original ijk coordinates.
+     */
+    public void neighbor(int digit) {
+        if (digit > Direction.CENTER_DIGIT.digit() && digit < Direction.NUM_DIGITS.digit()) {
+            ijkAdd(UNIT_VECS[digit][0], UNIT_VECS[digit][1], UNIT_VECS[digit][2]);
+            ijkNormalize();
+        }
+    }
+
+    /**
+     * Find the normalized ijk coordinates of the indexing parent of a cell in a
+     * clockwise aperture 7 grid.
+     */
+    public void upAp7r() {
+        i = this.i - this.k;
+        j = this.j - this.k;
+        int i = (int) Math.round((2 * this.i + this.j) / 7.0);
+        int j = (int) Math.round((3 * this.j - this.i) / 7.0);
+        this.i = i;
+        this.j = j;
+        this.k = 0;
+        ijkNormalize();
+    }
+
+    /**
+     * Find the normalized ijk coordinates of the indexing parent of a cell in a
+     * counter-clockwise aperture 7 grid.
+     *
+     */
+    public void upAp7() {
+        i = this.i - this.k;
+        j = this.j - this.k;
+        int i = (int) Math.round((3 * this.i - this.j) / 7.0);
+        int j = (int) Math.round((this.i + 2 * this.j) / 7.0);
+        this.i = i;
+        this.j = j;
+        this.k = 0;
+        ijkNormalize();
+    }
+
+    /**
+     * Determines the H3 digit corresponding to a unit vector in ijk coordinates.
+     *
+     * @return The H3 digit (0-6) corresponding to the ijk unit vector, or
+     * INVALID_DIGIT on failure.
+     */
+    public int unitIjkToDigit() {
+        ijkNormalize();
+        int digit = Direction.INVALID_DIGIT.digit();
+        for (int i = Direction.CENTER_DIGIT.digit(); i < Direction.NUM_DIGITS.digit(); i++) {
+            if (ijkMatches(UNIT_VECS[i])) {
+                digit = i;
+                break;
+            }
+        }
+        return digit;
+    }
+
+    /**
+     * Returns whether or not two ijk coordinates contain exactly the same
+     * component values.
+     *
+     * @param c The  set of ijk coordinates.
+     * @return true if the two addresses match, 0 if they do not.
+     */
+    private boolean ijkMatches(int[] c) {
+        return (i == c[0] && j == c[1] && k == c[2]);
+    }
+
+    /**
+     * Rotates indexing digit 60 degrees clockwise. Returns result.
+     *
+     * @param digit Indexing digit (between 1 and 6 inclusive)
+     */
+    public static int rotate60cw(int digit) {
+        switch (digit) {
+            case 1: // K_AXES_DIGIT
+                return Direction.JK_AXES_DIGIT.digit();
+            case 3: // JK_AXES_DIGIT:
+                return Direction.J_AXES_DIGIT.digit();
+            case 2: // J_AXES_DIGIT:
+                return Direction.IJ_AXES_DIGIT.digit();
+            case 6: // IJ_AXES_DIGIT
+                return Direction.I_AXES_DIGIT.digit();
+            case 4: // I_AXES_DIGIT
+                return Direction.IK_AXES_DIGIT.digit();
+            case 5: // IK_AXES_DIGIT
+                return Direction.K_AXES_DIGIT.digit();
+            default:
+                return digit;
+        }
+    }
+
+    /**
+     * Rotates indexing digit 60 degrees counter-clockwise. Returns result.
+     *
+     * @param digit Indexing digit (between 1 and 6 inclusive)
+     */
+    public static int rotate60ccw(int digit) {
+        switch (digit) {
+            case 1: // K_AXES_DIGIT
+                return Direction.IK_AXES_DIGIT.digit();
+            case 5: // IK_AXES_DIGIT
+                return Direction.I_AXES_DIGIT.digit();
+            case 4: // I_AXES_DIGIT
+                return Direction.IJ_AXES_DIGIT.digit();
+            case 6: // IJ_AXES_DIGIT
+                return Direction.J_AXES_DIGIT.digit();
+            case 2: // J_AXES_DIGIT:
+                return Direction.JK_AXES_DIGIT.digit();
+            case 3: // JK_AXES_DIGIT:
+                return Direction.K_AXES_DIGIT.digit();
+            default:
+                return digit;
+        }
+    }
+
+}

+ 813 - 0
libs/h3/src/main/java/org/elasticsearch/h3/FaceIJK.java

@@ -0,0 +1,813 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.
+ *
+ * Copyright 2016-2021 Uber Technologies, Inc.
+ */
+package org.elasticsearch.h3;
+
+/**
+ * Mutable face number and ijk coordinates on that face-centered coordinate system.
+ *
+ *  References the Vec2d cartesian coordinate systems hex2d: local face-centered
+ *  coordinate system scaled a specific H3 grid resolution unit length and
+ *  with x-axes aligned with the local i-axes
+ */
+final class FaceIJK {
+
+    /** enum representing overage type */
+    enum Overage {
+        /**
+         * Digit representing overage type
+         */
+        NO_OVERAGE,
+        /**
+         * On face edge (only occurs on substrate grids)
+         */
+        FACE_EDGE,
+        /**
+         * Overage on new face interior
+         */
+        NEW_FACE
+    }
+
+    // indexes for faceNeighbors table
+    /**
+     * IJ quadrant faceNeighbors table direction
+     */
+    private static final int IJ = 1;
+    /**
+     * KI quadrant faceNeighbors table direction
+     */
+    private static final int KI = 2;
+    /**
+     * JK quadrant faceNeighbors table direction
+     */
+    private static final int JK = 3;
+
+    /**
+     * overage distance table
+     */
+    private static final int[] maxDimByCIIres = {
+        2,        // res 0
+        -1,       // res 1
+        14,       // res 2
+        -1,       // res 3
+        98,       // res 4
+        -1,       // res 5
+        686,      // res 6
+        -1,       // res 7
+        4802,     // res 8
+        -1,       // res 9
+        33614,    // res 10
+        -1,       // res 11
+        235298,   // res 12
+        -1,       // res 13
+        1647086,  // res 14
+        -1,       // res 15
+        11529602  // res 16
+    };
+
+    /**
+     * unit scale distance table
+     */
+    private static final int[] unitScaleByCIIres = {
+        1,       // res 0
+        -1,      // res 1
+        7,       // res 2
+        -1,      // res 3
+        49,      // res 4
+        -1,      // res 5
+        343,     // res 6
+        -1,      // res 7
+        2401,    // res 8
+        -1,      // res 9
+        16807,   // res 10
+        -1,      // res 11
+        117649,  // res 12
+        -1,      // res 13
+        823543,  // res 14
+        -1,      // res 15
+        5764801  // res 16
+    };
+
+    /**
+     * direction from the origin face to the destination face, relative to
+     * the origin face's coordinate system, or -1 if not adjacent.
+     */
+    private static final int[][] adjacentFaceDir = new int[][] {
+        { 0, KI, -1, -1, IJ, JK, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },  // face 0
+        { IJ, 0, KI, -1, -1, -1, JK, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },  // face 1
+        { -1, IJ, 0, KI, -1, -1, -1, JK, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },  // face 2
+        { -1, -1, IJ, 0, KI, -1, -1, -1, JK, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },  // face 3
+        { KI, -1, -1, IJ, 0, -1, -1, -1, -1, JK, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },  // face 4
+        { JK, -1, -1, -1, -1, 0, -1, -1, -1, -1, IJ, -1, -1, -1, KI, -1, -1, -1, -1, -1 },  // face 5
+        { -1, JK, -1, -1, -1, -1, 0, -1, -1, -1, KI, IJ, -1, -1, -1, -1, -1, -1, -1, -1 },  // face 6
+        { -1, -1, JK, -1, -1, -1, -1, 0, -1, -1, -1, KI, IJ, -1, -1, -1, -1, -1, -1, -1 },  // face 7
+        { -1, -1, -1, JK, -1, -1, -1, -1, 0, -1, -1, -1, KI, IJ, -1, -1, -1, -1, -1, -1 },  // face 8
+        { -1, -1, -1, -1, JK, -1, -1, -1, -1, 0, -1, -1, -1, KI, IJ, -1, -1, -1, -1, -1 },  // face 9
+        { -1, -1, -1, -1, -1, IJ, KI, -1, -1, -1, 0, -1, -1, -1, -1, JK, -1, -1, -1, -1 },  // face 10
+        { -1, -1, -1, -1, -1, -1, IJ, KI, -1, -1, -1, 0, -1, -1, -1, -1, JK, -1, -1, -1 },  // face 11
+        { -1, -1, -1, -1, -1, -1, -1, IJ, KI, -1, -1, -1, 0, -1, -1, -1, -1, JK, -1, -1 },  // face 12
+        { -1, -1, -1, -1, -1, -1, -1, -1, IJ, KI, -1, -1, -1, 0, -1, -1, -1, -1, JK, -1 },  // face 13
+        { -1, -1, -1, -1, -1, KI, -1, -1, -1, IJ, -1, -1, -1, -1, 0, -1, -1, -1, -1, JK },  // face 14
+        { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, JK, -1, -1, -1, -1, 0, IJ, -1, -1, KI },  // face 15
+        { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, JK, -1, -1, -1, KI, 0, IJ, -1, -1 },  // face 16
+        { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, JK, -1, -1, -1, KI, 0, IJ, -1 },  // face 17
+        { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, JK, -1, -1, -1, KI, 0, IJ },  // face 18
+        { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, JK, IJ, -1, -1, KI, 0 }  // face 19
+    };
+
+    /** Maximum input for any component to face-to-base-cell lookup functions */
+    private static final int MAX_FACE_COORD = 2;
+
+    /**
+     *  Information to transform into an adjacent face IJK system
+     */
+    private static class FaceOrientIJK {
+        // face number
+        final int face;
+        // res 0 translation relative to primary face
+        final int translateI;
+        final int translateJ;
+        final int translateK;
+        // number of 60 degree ccw rotations relative to primary
+        final int ccwRot60;
+
+        // face
+        FaceOrientIJK(int face, int translateI, int translateJ, int translateK, int ccwRot60) {
+            this.face = face;
+            this.translateI = translateI;
+            this.translateJ = translateJ;
+            this.translateK = translateK;
+            this.ccwRot60 = ccwRot60;
+        }
+    }
+
+    /**
+     *  Definition of which faces neighbor each other.
+     */
+    private static final FaceOrientIJK[][] faceNeighbors = new FaceOrientIJK[][] {
+        {
+            // face 0
+            new FaceOrientIJK(0, 0, 0, 0, 0),  // central face
+            new FaceOrientIJK(4, 2, 0, 2, 1),  // ij quadrant
+            new FaceOrientIJK(1, 2, 2, 0, 5),  // ki quadrant
+            new FaceOrientIJK(5, 0, 2, 2, 3)   // jk quadrant
+        },
+        {
+            // face 1
+            new FaceOrientIJK(1, 0, 0, 0, 0),  // central face
+            new FaceOrientIJK(0, 2, 0, 2, 1),  // ij quadrant
+            new FaceOrientIJK(2, 2, 2, 0, 5),  // ki quadrant
+            new FaceOrientIJK(6, 0, 2, 2, 3)   // jk quadrant
+        },
+        {
+            // face 2
+            new FaceOrientIJK(2, 0, 0, 0, 0),  // central face
+            new FaceOrientIJK(1, 2, 0, 2, 1),  // ij quadrant
+            new FaceOrientIJK(3, 2, 2, 0, 5),  // ki quadrant
+            new FaceOrientIJK(7, 0, 2, 2, 3)   // jk quadrant
+        },
+        {
+            // face 3
+            new FaceOrientIJK(3, 0, 0, 0, 0),  // central face
+            new FaceOrientIJK(2, 2, 0, 2, 1),  // ij quadrant
+            new FaceOrientIJK(4, 2, 2, 0, 5),  // ki quadrant
+            new FaceOrientIJK(8, 0, 2, 2, 3)   // jk quadrant
+        },
+        {
+            // face 4
+            new FaceOrientIJK(4, 0, 0, 0, 0),  // central face
+            new FaceOrientIJK(3, 2, 0, 2, 1),  // ij quadrant
+            new FaceOrientIJK(0, 2, 2, 0, 5),  // ki quadrant
+            new FaceOrientIJK(9, 0, 2, 2, 3)   // jk quadrant
+        },
+        {
+            // face 5
+            new FaceOrientIJK(5, 0, 0, 0, 0),   // central face
+            new FaceOrientIJK(10, 2, 2, 0, 3),  // ij quadrant
+            new FaceOrientIJK(14, 2, 0, 2, 3),  // ki quadrant
+            new FaceOrientIJK(0, 0, 2, 2, 3)    // jk quadrant
+        },
+        {
+            // face 6
+            new FaceOrientIJK(6, 0, 0, 0, 0),   // central face
+            new FaceOrientIJK(11, 2, 2, 0, 3),  // ij quadrant
+            new FaceOrientIJK(10, 2, 0, 2, 3),  // ki quadrant
+            new FaceOrientIJK(1, 0, 2, 2, 3)    // jk quadrant
+        },
+        {
+            // face 7
+            new FaceOrientIJK(7, 0, 0, 0, 0),   // central face
+            new FaceOrientIJK(12, 2, 2, 0, 3),  // ij quadrant
+            new FaceOrientIJK(11, 2, 0, 2, 3),  // ki quadrant
+            new FaceOrientIJK(2, 0, 2, 2, 3)    // jk quadrant
+        },
+        {
+            // face 8
+            new FaceOrientIJK(8, 0, 0, 0, 0),   // central face
+            new FaceOrientIJK(13, 2, 2, 0, 3),  // ij quadrant
+            new FaceOrientIJK(12, 2, 0, 2, 3),  // ki quadrant
+            new FaceOrientIJK(3, 0, 2, 2, 3)    // jk quadrant
+        },
+        {
+            // face 9
+            new FaceOrientIJK(9, 0, 0, 0, 0),   // central face
+            new FaceOrientIJK(14, 2, 2, 0, 3),  // ij quadrant
+            new FaceOrientIJK(13, 2, 0, 2, 3),  // ki quadrant
+            new FaceOrientIJK(4, 0, 2, 2, 3)   // jk quadrant
+        },
+        {
+            // face 10
+            new FaceOrientIJK(10, 0, 0, 0, 0),  // central face
+            new FaceOrientIJK(5, 2, 2, 0, 3),   // ij quadrant
+            new FaceOrientIJK(6, 2, 0, 2, 3),   // ki quadrant
+            new FaceOrientIJK(15, 0, 2, 2, 3)   // jk quadrant
+        },
+        {
+            // face 11
+            new FaceOrientIJK(11, 0, 0, 0, 0),  // central face
+            new FaceOrientIJK(6, 2, 2, 0, 3),   // ij quadrant
+            new FaceOrientIJK(7, 2, 0, 2, 3),   // ki quadrant
+            new FaceOrientIJK(16, 0, 2, 2, 3)   // jk quadrant
+        },
+        {
+            // face 12
+            new FaceOrientIJK(12, 0, 0, 0, 0),  // central face
+            new FaceOrientIJK(7, 2, 2, 0, 3),   // ij quadrant
+            new FaceOrientIJK(8, 2, 0, 2, 3),   // ki quadrant
+            new FaceOrientIJK(17, 0, 2, 2, 3)   // jk quadrant
+        },
+        {
+            // face 13
+            new FaceOrientIJK(13, 0, 0, 0, 0),  // central face
+            new FaceOrientIJK(8, 2, 2, 0, 3),   // ij quadrant
+            new FaceOrientIJK(9, 2, 0, 2, 3),   // ki quadrant
+            new FaceOrientIJK(18, 0, 2, 2, 3)   // jk quadrant
+        },
+        {
+            // face 14
+            new FaceOrientIJK(14, 0, 0, 0, 0),  // central face
+            new FaceOrientIJK(9, 2, 2, 0, 3),   // ij quadrant
+            new FaceOrientIJK(5, 2, 0, 2, 3),   // ki quadrant
+            new FaceOrientIJK(19, 0, 2, 2, 3)   // jk quadrant
+        },
+        {
+            // face 15
+            new FaceOrientIJK(15, 0, 0, 0, 0),  // central face
+            new FaceOrientIJK(16, 2, 0, 2, 1),  // ij quadrant
+            new FaceOrientIJK(19, 2, 2, 0, 5),  // ki quadrant
+            new FaceOrientIJK(10, 0, 2, 2, 3)   // jk quadrant
+        },
+        {
+            // face 16
+            new FaceOrientIJK(16, 0, 0, 0, 0),  // central face
+            new FaceOrientIJK(17, 2, 0, 2, 1),  // ij quadrant
+            new FaceOrientIJK(15, 2, 2, 0, 5),  // ki quadrant
+            new FaceOrientIJK(11, 0, 2, 2, 3)   // jk quadrant
+        },
+        {
+            // face 17
+            new FaceOrientIJK(17, 0, 0, 0, 0),  // central face
+            new FaceOrientIJK(18, 2, 0, 2, 1),  // ij quadrant
+            new FaceOrientIJK(16, 2, 2, 0, 5),  // ki quadrant
+            new FaceOrientIJK(12, 0, 2, 2, 3)   // jk quadrant
+        },
+        {
+            // face 18
+            new FaceOrientIJK(18, 0, 0, 0, 0),  // central face
+            new FaceOrientIJK(19, 2, 0, 2, 1),  // ij quadrant
+            new FaceOrientIJK(17, 2, 2, 0, 5),  // ki quadrant
+            new FaceOrientIJK(13, 0, 2, 2, 3)   // jk quadrant
+        },
+        {
+            // face 19
+            new FaceOrientIJK(19, 0, 0, 0, 0),  // central face
+            new FaceOrientIJK(15, 2, 0, 2, 1),  // ij quadrant
+            new FaceOrientIJK(18, 2, 2, 0, 5),  // ki quadrant
+            new FaceOrientIJK(14, 0, 2, 2, 3)   // jk quadrant
+        } };
+
+    int face;        // face number
+    CoordIJK coord;  // ijk coordinates on that face
+
+    FaceIJK(int face, CoordIJK coord) {
+        this.face = face;
+        this.coord = coord;
+    }
+
+    /**
+     * Adjusts this FaceIJK address so that the resulting cell address is
+     * relative to the correct icosahedral face.
+     *
+     * @param res          The H3 resolution of the cell.
+     * @param pentLeading4 Whether or not the cell is a pentagon with a leading
+     *                     digit 4.
+     * @param substrate    Whether or not the cell is in a substrate grid.
+     * @return 0 if on original face (no overage); 1 if on face edge (only occurs
+     * on substrate grids); 2 if overage on new face interior
+     */
+    public Overage adjustOverageClassII(int res, boolean pentLeading4, boolean substrate) {
+        Overage overage = Overage.NO_OVERAGE;
+        // get the maximum dimension value; scale if a substrate grid
+        int maxDim = maxDimByCIIres[res];
+        if (substrate) {
+            maxDim *= 3;
+        }
+
+        // check for overage
+        if (substrate && this.coord.i + this.coord.j + this.coord.k == maxDim) { // on edge
+            overage = Overage.FACE_EDGE;
+        } else if (this.coord.i + this.coord.j + this.coord.k > maxDim) { // overage
+            overage = Overage.NEW_FACE;
+            final FaceOrientIJK fijkOrient;
+            if (this.coord.k > 0) {
+                if (this.coord.j > 0) { // jk "quadrant"
+                    fijkOrient = faceNeighbors[this.face][JK];
+                } else { // ik "quadrant"
+                    fijkOrient = faceNeighbors[this.face][KI];
+                    // adjust for the pentagonal missing sequence
+                    if (pentLeading4) {
+                        // translate origin to center of pentagon
+                        this.coord.ijkSub(maxDim, 0, 0);
+                        // rotate to adjust for the missing sequence
+                        this.coord.ijkRotate60cw();
+                        // translate the origin back to the center of the triangle
+                        this.coord.ijkAdd(maxDim, 0, 0);
+                    }
+                }
+            } else { // ij "quadrant"
+                fijkOrient = faceNeighbors[this.face][IJ];
+            }
+
+            this.face = fijkOrient.face;
+
+            // rotate and translate for adjacent face
+            for (int i = 0; i < fijkOrient.ccwRot60; i++) {
+                this.coord.ijkRotate60ccw();
+            }
+
+            int unitScale = unitScaleByCIIres[res];
+            if (substrate) {
+                unitScale *= 3;
+            }
+            this.coord.ijkAdd(fijkOrient.translateI * unitScale, fijkOrient.translateJ * unitScale, fijkOrient.translateK * unitScale);
+            this.coord.ijkNormalize();
+
+            // overage points on pentagon boundaries can end up on edges
+            if (substrate && this.coord.i + this.coord.j + this.coord.k == maxDim) { // on edge
+                overage = Overage.FACE_EDGE;
+            }
+        }
+        return overage;
+    }
+
+    /**
+     * Computes the center point in spherical coordinates of a cell given by
+     * a FaceIJK address at a specified resolution.
+     *
+     * @param res The H3 resolution of the cell.
+     */
+    public LatLng faceIjkToGeo(int res) {
+        Vec2d v = coord.ijkToHex2d();
+        return v.hex2dToGeo(face, res, false);
+    }
+
+    /**
+     * Computes the cell boundary in spherical coordinates for a pentagonal cell
+     * for this FaceIJK address at a specified resolution.
+     *
+     * @param res    The H3 resolution of the cell.
+     * @param start  The first topological vertex to return.
+     * @param length The number of topological vertexes to return.
+     */
+    public CellBoundary faceIjkPentToCellBoundary(int res, int start, int length) {
+        FaceIJK[] fijkVerts = new FaceIJK[Constants.NUM_PENT_VERTS];
+        int adjRes = faceIjkPentToVerts(res, fijkVerts);
+
+        // If we're returning the entire loop, we need one more iteration in case
+        // of a distortion vertex on the last edge
+        int additionalIteration = length == Constants.NUM_PENT_VERTS ? 1 : 0;
+
+        // convert each vertex to lat/lng
+        // adjust the face of each vertex as appropriate and introduce
+        // edge-crossing vertices as needed
+        CellBoundary boundary = new CellBoundary();
+        FaceIJK lastFijk = null;
+        for (int vert = start; vert < start + length + additionalIteration; vert++) {
+            int v = vert % Constants.NUM_PENT_VERTS;
+
+            FaceIJK fijk = fijkVerts[v];
+
+            fijk.adjustPentVertOverage(adjRes);
+
+            // all Class III pentagon edges cross icosa edges
+            // note that Class II pentagons have vertices on the edge,
+            // not edge intersections
+            if (H3Index.isResolutionClassIII(res) && vert > start) {
+                // find hex2d of the two vertexes on the last face
+                FaceIJK tmpFijk = new FaceIJK(fijk.face, new CoordIJK(fijk.coord.i, fijk.coord.j, fijk.coord.k));
+
+                Vec2d orig2d0 = lastFijk.coord.ijkToHex2d();
+
+                int currentToLastDir = adjacentFaceDir[tmpFijk.face][lastFijk.face];
+
+                FaceOrientIJK fijkOrient = faceNeighbors[tmpFijk.face][currentToLastDir];
+
+                tmpFijk.face = fijkOrient.face;
+                CoordIJK ijk = tmpFijk.coord;
+
+                // rotate and translate for adjacent face
+                for (int i = 0; i < fijkOrient.ccwRot60; i++) {
+                    ijk.ijkRotate60ccw();
+                }
+
+                int unitScale = unitScaleByCIIres[adjRes] * 3;
+                ijk.ijkAdd(fijkOrient.translateI * unitScale, fijkOrient.translateJ * unitScale, fijkOrient.translateK * unitScale);
+                ijk.ijkNormalize();
+
+                Vec2d orig2d1 = ijk.ijkToHex2d();
+
+                // find the appropriate icosa face edge vertexes
+                int maxDim = maxDimByCIIres[adjRes];
+                Vec2d v0 = new Vec2d(3.0 * maxDim, 0.0);
+                Vec2d v1 = new Vec2d(-1.5 * maxDim, 3.0 * Constants.M_SQRT3_2 * maxDim);
+                Vec2d v2 = new Vec2d(-1.5 * maxDim, -3.0 * Constants.M_SQRT3_2 * maxDim);
+
+                Vec2d edge0;
+                Vec2d edge1;
+                switch (adjacentFaceDir[tmpFijk.face][fijk.face]) {
+                    case IJ:
+                        edge0 = v0;
+                        edge1 = v1;
+                        break;
+                    case JK:
+                        edge0 = v1;
+                        edge1 = v2;
+                        break;
+                    case KI:
+                    default:
+                        assert (adjacentFaceDir[tmpFijk.face][fijk.face] == KI);
+                        edge0 = v2;
+                        edge1 = v0;
+                        break;
+                }
+
+                // find the intersection and add the lat/lng point to the result
+                Vec2d inter = Vec2d.v2dIntersect(orig2d0, orig2d1, edge0, edge1);
+                LatLng point = inter.hex2dToGeo(tmpFijk.face, adjRes, true);
+                boundary.add(point);
+            }
+
+            // convert vertex to lat/lng and add to the result
+            // vert == start + NUM_PENT_VERTS is only used to test for possible
+            // intersection on last edge
+            if (vert < start + Constants.NUM_PENT_VERTS) {
+                Vec2d vec = fijk.coord.ijkToHex2d();
+                LatLng point = vec.hex2dToGeo(fijk.face, adjRes, true);
+                boundary.add(point);
+            }
+
+            lastFijk = fijk;
+        }
+        return boundary;
+    }
+
+    /**
+     * Generates the cell boundary in spherical coordinates for a cell given by this
+     * FaceIJK address at a specified resolution.
+     *
+     * @param res    The H3 resolution of the cell.
+     * @param start  The first topological vertex to return.
+     * @param length The number of topological vertexes to return.
+     */
+    public CellBoundary faceIjkToCellBoundary(int res, int start, int length) {
+        FaceIJK fijkVerts[] = new FaceIJK[Constants.NUM_HEX_VERTS];
+        int adjRes = faceIjkToVerts(res, fijkVerts);
+        // If we're returning the entire loop, we need one more iteration in case
+        // of a distortion vertex on the last edge
+        int additionalIteration = length == Constants.NUM_HEX_VERTS ? 1 : 0;
+
+        // convert each vertex to lat/lng
+        // adjust the face of each vertex as appropriate and introduce
+        // edge-crossing vertices as needed
+        CellBoundary boundary = new CellBoundary();
+        int lastFace = -1;
+        Overage lastOverage = Overage.NO_OVERAGE;
+        for (int vert = start; vert < start + length + additionalIteration; vert++) {
+            int v = vert % Constants.NUM_HEX_VERTS;
+
+            FaceIJK fijk = new FaceIJK(fijkVerts[v].face, new CoordIJK(fijkVerts[v].coord.i, fijkVerts[v].coord.j, fijkVerts[v].coord.k));
+
+            //
+            final boolean pentLeading4 = false; // may change in c code when calling method
+            Overage overage = fijk.adjustOverageClassII(adjRes, pentLeading4, true);
+
+            /*
+            Check for edge-crossing. Each face of the underlying icosahedron is a
+            different projection plane. So if an edge of the hexagon crosses an
+            icosahedron edge, an additional vertex must be introduced at that
+            intersection point. Then each half of the cell edge can be projected
+            to geographic coordinates using the appropriate icosahedron face
+            projection. Note that Class II cell edges have vertices on the face
+            edge, with no edge line intersections.
+            */
+            if (H3Index.isResolutionClassIII(res) && vert > start && fijk.face != lastFace && lastOverage != Overage.FACE_EDGE) {
+                // find hex2d of the two vertexes on original face
+                int lastV = (v + 5) % Constants.NUM_HEX_VERTS;
+                Vec2d orig2d0 = fijkVerts[lastV].coord.ijkToHex2d();
+                Vec2d orig2d1 = fijkVerts[v].coord.ijkToHex2d();
+
+                // find the appropriate icosa face edge vertexes
+                int maxDim = maxDimByCIIres[adjRes];
+                Vec2d v0 = new Vec2d(3.0 * maxDim, 0.0);
+                Vec2d v1 = new Vec2d(-1.5 * maxDim, 3.0 * Constants.M_SQRT3_2 * maxDim);
+                Vec2d v2 = new Vec2d(-1.5 * maxDim, -3.0 * Constants.M_SQRT3_2 * maxDim);
+
+                int face2 = ((lastFace == this.face) ? fijk.face : lastFace);
+                final Vec2d edge0;
+                final Vec2d edge1;
+                switch (adjacentFaceDir[this.face][face2]) {
+                    case IJ:
+                        edge0 = v0;
+                        edge1 = v1;
+                        break;
+                    case JK:
+                        edge0 = v1;
+                        edge1 = v2;
+                        break;
+                    // case KI:
+                    default:
+                        assert (adjacentFaceDir[this.face][face2] == KI);
+                        edge0 = v2;
+                        edge1 = v0;
+                        break;
+                }
+
+                // find the intersection and add the lat/lng point to the result
+                Vec2d inter = Vec2d.v2dIntersect(orig2d0, orig2d1, edge0, edge1);
+                /*
+                If a point of intersection occurs at a hexagon vertex, then each
+                adjacent hexagon edge will lie completely on a single icosahedron
+                face, and no additional vertex is required.
+                */
+                boolean isIntersectionAtVertex = orig2d0.equals(inter) || orig2d1.equals(inter);
+                if (isIntersectionAtVertex == false) {
+                    LatLng point = inter.hex2dToGeo(this.face, adjRes, true);
+                    boundary.add(point);
+                }
+            }
+
+            // convert vertex to lat/lng and add to the result
+            // vert == start + NUM_HEX_VERTS is only used to test for possible
+            // intersection on last edge
+            if (vert < start + Constants.NUM_HEX_VERTS) {
+                Vec2d vec = fijk.coord.ijkToHex2d();
+                LatLng point = vec.hex2dToGeo(fijk.face, adjRes, true);
+                boundary.add(point);
+            }
+            lastFace = fijk.face;
+            lastOverage = overage;
+        }
+        return boundary;
+    }
+
+    /**
+     * compute the corresponding H3Index.
+     * @param res The cell resolution.
+     * @return The encoded H3Index (or H3_NULL on failure).
+     */
+    public long faceIjkToH3(int res) {
+        // initialize the index
+        long h = H3Index.H3_INIT;
+        h = H3Index.H3_set_mode(h, Constants.H3_CELL_MODE);
+        h = H3Index.H3_set_resolution(h, res);
+
+        // check for res 0/base cell
+        if (res == 0) {
+            if (coord.i > MAX_FACE_COORD || coord.j > MAX_FACE_COORD || coord.k > MAX_FACE_COORD) {
+                // out of range input
+                throw new IllegalArgumentException(" out of range input");
+            }
+
+            return H3Index.H3_set_base_cell(h, BaseCells.getBaseCell(this));
+        }
+
+        // we need to find the correct base cell FaceIJK for this H3 index;
+        // start with the passed in face and resolution res ijk coordinates
+        // in that face's coordinate system
+
+        // build the H3Index from finest res up
+        // adjust r for the fact that the res 0 base cell offsets the indexing
+        // digits
+        for (int r = res - 1; r >= 0; r--) {
+            int lastI = coord.i;
+            int lastJ = coord.j;
+            int lastK = coord.k;
+            CoordIJK lastCenter;
+            if (H3Index.isResolutionClassIII(r + 1)) {
+                // rotate ccw
+                coord.upAp7();
+                lastCenter = new CoordIJK(coord.i, coord.j, coord.k);
+                lastCenter.downAp7();
+            } else {
+                // rotate cw
+                coord.upAp7r();
+                lastCenter = new CoordIJK(coord.i, coord.j, coord.k);
+                lastCenter.downAp7r();
+            }
+
+            CoordIJK diff = new CoordIJK(lastI - lastCenter.i, lastJ - lastCenter.j, lastK - lastCenter.k);
+            diff.ijkNormalize();
+            h = H3Index.H3_set_index_digit(h, r + 1, diff.unitIjkToDigit());
+        }
+
+        // we should now hold the IJK of the base cell in the
+        // coordinate system of the current face
+
+        if (coord.i > MAX_FACE_COORD || coord.j > MAX_FACE_COORD || coord.k > MAX_FACE_COORD) {
+            // out of range input
+            throw new IllegalArgumentException(" out of range input");
+        }
+
+        // lookup the correct base cell
+        int baseCell = BaseCells.getBaseCell(this);
+        h = H3Index.H3_set_base_cell(h, baseCell);
+
+        // rotate if necessary to get canonical base cell orientation
+        // for this base cell
+        int numRots = BaseCells.getBaseCellCCWrot60(this);
+        if (BaseCells.isBaseCellPentagon(baseCell)) {
+            // force rotation out of missing k-axes sub-sequence
+            if (H3Index.h3LeadingNonZeroDigit(h) == CoordIJK.Direction.K_AXES_DIGIT.digit()) {
+                // check for a cw/ccw offset face; default is ccw
+                if (BaseCells.baseCellIsCwOffset(baseCell, face)) {
+                    h = H3Index.h3Rotate60cw(h);
+                } else {
+                    h = H3Index.h3Rotate60ccw(h);
+                }
+            }
+
+            for (int i = 0; i < numRots; i++) {
+                h = H3Index.h3RotatePent60ccw(h);
+            }
+        } else {
+            for (int i = 0; i < numRots; i++) {
+                h = H3Index.h3Rotate60ccw(h);
+            }
+        }
+
+        return h;
+    }
+
+    /**
+     * Populate the vertices of this cell as substrate FaceIJK addresses.
+     *
+     * @param res The H3 resolution of the cell. This may be adjusted if
+     *            necessary for the substrate grid resolution.
+     */
+    private int faceIjkToVerts(int res, FaceIJK[] fijkVerts) {
+        // get the correct set of substrate vertices for this resolution
+        CoordIJK[] verts;
+        if (H3Index.isResolutionClassIII(res)) {
+            // the vertexes of an origin-centered cell in a Class III resolution on a
+            // substrate grid with aperture sequence 33r7r. The aperture 3 gets us the
+            // vertices, and the 3r7r gets us to Class II.
+            // vertices listed ccw from the i-axes
+            verts = new CoordIJK[] {
+                new CoordIJK(5, 4, 0),  // 0
+                new CoordIJK(1, 5, 0),  // 1
+                new CoordIJK(0, 5, 4),  // 2
+                new CoordIJK(0, 1, 5),  // 3
+                new CoordIJK(4, 0, 5),  // 4
+                new CoordIJK(5, 0, 1)   // 5
+            };
+        } else {
+            // the vertexes of an origin-centered cell in a Class II resolution on a
+            // substrate grid with aperture sequence 33r. The aperture 3 gets us the
+            // vertices, and the 3r gets us back to Class II.
+            // vertices listed ccw from the i-axes
+            verts = new CoordIJK[] {
+                new CoordIJK(2, 1, 0),  // 0
+                new CoordIJK(1, 2, 0),  // 1
+                new CoordIJK(0, 2, 1),  // 2
+                new CoordIJK(0, 1, 2),  // 3
+                new CoordIJK(1, 0, 2),  // 4
+                new CoordIJK(2, 0, 1)   // 5
+            };
+        }
+
+        // adjust the center point to be in an aperture 33r substrate grid
+        // these should be composed for speed
+        this.coord.downAp3();
+        this.coord.downAp3r();
+
+        // if res is Class III we need to add a cw aperture 7 to get to
+        // icosahedral Class II
+        if (H3Index.isResolutionClassIII(res)) {
+            this.coord.downAp7r();
+            res += 1;
+        }
+
+        // The center point is now in the same substrate grid as the origin
+        // cell vertices. Add the center point substate coordinates
+        // to each vertex to translate the vertices to that cell.
+
+        for (int v = 0; v < Constants.NUM_HEX_VERTS; v++) {
+            verts[v].ijkAdd(this.coord.i, this.coord.j, this.coord.k);
+            verts[v].ijkNormalize();
+            fijkVerts[v] = new FaceIJK(this.face, verts[v]);
+        }
+        return res;
+    }
+
+    /**
+     * Populate the vertices of this pentagon cell as substrate FaceIJK addresses
+     *
+     * @param res The H3 resolution of the cell. This may be adjusted if
+     *            necessary for the substrate grid resolution.
+     */
+    private int faceIjkPentToVerts(int res, FaceIJK[] fijkVerts) {
+        // get the correct set of substrate vertices for this resolution
+        CoordIJK[] verts;
+        if (H3Index.isResolutionClassIII(res)) {
+            // the vertexes of an origin-centered pentagon in a Class II resolution on a
+            // substrate grid with aperture sequence 33r. The aperture 3 gets us the
+            // vertices, and the 3r gets us back to Class II.
+            // vertices listed ccw from the i-axes
+            verts = new CoordIJK[] {
+                new CoordIJK(5, 4, 0),  // 0
+                new CoordIJK(1, 5, 0),  // 1
+                new CoordIJK(0, 5, 4),  // 2
+                new CoordIJK(0, 1, 5),  // 3
+                new CoordIJK(4, 0, 5)  // 4
+            };
+        } else {
+            // the vertexes of an origin-centered pentagon in a Class III resolution on
+            // a substrate grid with aperture sequence 33r7r. The aperture 3 gets us the
+            // vertices, and the 3r7r gets us to Class II. vertices listed ccw from the
+            // i-axes
+            verts = new CoordIJK[] {
+                new CoordIJK(2, 1, 0),  // 0
+                new CoordIJK(1, 2, 0),  // 1
+                new CoordIJK(0, 2, 1),  // 2
+                new CoordIJK(0, 1, 2),  // 3
+                new CoordIJK(1, 0, 2)  // 4
+            };
+        }
+
+        // adjust the center point to be in an aperture 33r substrate grid
+        // these should be composed for speed
+        this.coord.downAp3();
+        this.coord.downAp3r();
+
+        // if res is Class III we need to add a cw aperture 7 to get to
+        // icosahedral Class II
+        if (H3Index.isResolutionClassIII(res)) {
+            this.coord.downAp7r();
+            res += 1;
+        }
+
+        // The center point is now in the same substrate grid as the origin
+        // cell vertices. Add the center point substate coordinates
+        // to each vertex to translate the vertices to that cell.
+        for (int v = 0; v < Constants.NUM_PENT_VERTS; v++) {
+            verts[v].ijkAdd(this.coord.i, this.coord.j, this.coord.k);
+            verts[v].ijkNormalize();
+            fijkVerts[v] = new FaceIJK(this.face, verts[v]);
+        }
+        return res;
+    }
+
+    /**
+     * Adjusts a FaceIJK address for a pentagon vertex in a substrate grid in
+     * place so that the resulting cell address is relative to the correct
+     * icosahedral face.
+     *
+     * @param res The H3 resolution of the cell.
+     */
+    private Overage adjustPentVertOverage(int res) {
+        Overage overage;
+        do {
+            overage = adjustOverageClassII(res, false, true);
+        } while (overage == Overage.NEW_FACE);
+        return overage;
+    }
+}

+ 306 - 0
libs/h3/src/main/java/org/elasticsearch/h3/H3.java

@@ -0,0 +1,306 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.
+ *
+ * Copyright 2016-2021 Uber Technologies, Inc.
+ */
+package org.elasticsearch.h3;
+
+import java.util.Arrays;
+
+import static java.lang.Math.toRadians;
+
+/**
+ * Defines the public API of the H3 library.
+ */
+public final class H3 {
+
+    public static int MAX_H3_RES = Constants.MAX_H3_RES;
+
+    /**
+     * Converts from <code>long</code> representation of an index to <code>String</code> representation.
+     */
+    public static String h3ToString(long h3) {
+        return Long.toHexString(h3);
+    }
+
+    /**
+     * Converts from <code>String</code> representation of an index to <code>long</code> representation.
+     */
+    public static long stringToH3(String h3Address) {
+        return Long.parseUnsignedLong(h3Address, 16);
+    }
+
+    /** determines if an H3 cell is a pentagon */
+    public static boolean isPentagon(long h3) {
+        return H3Index.H3_is_pentagon(h3);
+    }
+
+    /** determines if an H3 cell in string format is a pentagon */
+    public static boolean isPentagon(String h3Address) {
+        return isPentagon(stringToH3(h3Address));
+    }
+
+    /** Returns true if this is a valid H3 index */
+    public static boolean h3IsValid(long h3) {
+        if (H3Index.H3_get_high_bit(h3) != 0) {
+            return false;
+        }
+
+        if (H3Index.H3_get_mode(h3) != Constants.H3_CELL_MODE) {
+            return false;
+        }
+
+        if (H3Index.H3_get_reserved_bits(h3) != 0) {
+            return false;
+        }
+
+        int baseCell = H3Index.H3_get_base_cell(h3);
+        if (baseCell < 0 || baseCell >= Constants.NUM_BASE_CELLS) {  // LCOV_EXCL_BR_LINE
+            // Base cells less than zero can not be represented in an index
+            return false;
+        }
+
+        int res = H3Index.H3_get_resolution(h3);
+        if (res < 0 || res > Constants.MAX_H3_RES) {  // LCOV_EXCL_BR_LINE
+            // Resolutions less than zero can not be represented in an index
+            return false;
+        }
+
+        boolean foundFirstNonZeroDigit = false;
+        for (int r = 1; r <= res; r++) {
+            int digit = H3Index.H3_get_index_digit(h3, r);
+
+            if (foundFirstNonZeroDigit == false && digit != CoordIJK.Direction.CENTER_DIGIT.digit()) {
+                foundFirstNonZeroDigit = true;
+                if (BaseCells.isBaseCellPentagon(baseCell) && digit == CoordIJK.Direction.K_AXES_DIGIT.digit()) {
+                    return false;
+                }
+            }
+
+            if (digit < CoordIJK.Direction.CENTER_DIGIT.digit() || digit >= CoordIJK.Direction.NUM_DIGITS.digit()) {
+                return false;
+            }
+        }
+
+        for (int r = res + 1; r <= Constants.MAX_H3_RES; r++) {
+            int digit = H3Index.H3_get_index_digit(h3, r);
+            if (digit != CoordIJK.Direction.INVALID_DIGIT.digit()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /** Returns true if this is a valid H3 index */
+    public static boolean h3IsValid(String h3Address) {
+        return h3IsValid(stringToH3(h3Address));
+    }
+
+    /**
+     * Return all base cells
+     */
+    public static long[] getLongRes0Cells() {
+        long[] cells = new long[Constants.NUM_BASE_CELLS];
+        for (int bc = 0; bc < Constants.NUM_BASE_CELLS; bc++) {
+            long baseCell = H3Index.H3_INIT;
+            baseCell = H3Index.H3_set_mode(baseCell, Constants.H3_CELL_MODE);
+            baseCell = H3Index.H3_set_base_cell(baseCell, bc);
+            cells[bc] = baseCell;
+        }
+        return cells;
+    }
+
+    /**
+     * Return all base cells
+     */
+    public static String[] getStringRes0Cells() {
+        return h3ToStringList(getLongRes0Cells());
+    }
+
+    /**
+     * Find the {@link LatLng} center point of the cell.
+     */
+    public static LatLng h3ToLatLng(long h3) {
+        final FaceIJK fijk = H3Index.h3ToFaceIjk(h3);
+        return fijk.faceIjkToGeo(H3Index.H3_get_resolution(h3));
+    }
+
+    /**
+     * Find the {@link LatLng}  center point of the cell.
+     */
+    public static LatLng h3ToLatLng(String h3Address) {
+        return h3ToLatLng(stringToH3(h3Address));
+    }
+
+    /**
+     * Find the cell {@link CellBoundary} coordinates for the cell
+     */
+    public static CellBoundary h3ToGeoBoundary(long h3) {
+        FaceIJK fijk = H3Index.h3ToFaceIjk(h3);
+        if (H3Index.H3_is_pentagon(h3)) {
+            return fijk.faceIjkPentToCellBoundary(H3Index.H3_get_resolution(h3), 0, Constants.NUM_PENT_VERTS);
+        } else {
+            return fijk.faceIjkToCellBoundary(H3Index.H3_get_resolution(h3), 0, Constants.NUM_HEX_VERTS);
+        }
+    }
+
+    /**
+     * Find the cell {@link CellBoundary} coordinates for the cell
+     */
+    public static CellBoundary h3ToGeoBoundary(String h3Address) {
+        return h3ToGeoBoundary(stringToH3(h3Address));
+    }
+
+    /**
+     * Find the H3 index of the resolution <code>res</code> cell containing the lat/lon (in degrees)
+     *
+     * @param lat Latitude in degrees.
+     * @param lng Longitude in degrees.
+     * @param res Resolution, 0 &lt;= res &lt;= 15
+     * @return The H3 index.
+     * @throws IllegalArgumentException latitude, longitude, or resolution are out of range.
+     */
+    public static long geoToH3(double lat, double lng, int res) {
+        checkResolution(res);
+        return new LatLng(toRadians(lat), toRadians(lng)).geoToFaceIJK(res).faceIjkToH3(res);
+    }
+
+    /**
+     * Find the H3 index of the resolution <code>res</code> cell containing the lat/lon (in degrees)
+     *
+     * @param lat Latitude in degrees.
+     * @param lng Longitude in degrees.
+     * @param res Resolution, 0 &lt;= res &lt;= 15
+     * @return The H3 index.
+     * @throws IllegalArgumentException Latitude, longitude, or resolution is out of range.
+     */
+    public static String geoToH3Address(double lat, double lng, int res) {
+        return h3ToString(geoToH3(lat, lng, res));
+    }
+
+    /**
+     * Returns the parent of the given index.
+     */
+    public static long h3ToParent(long h3) {
+        int childRes = H3Index.H3_get_resolution(h3);
+        if (childRes == 0) {
+            throw new IllegalArgumentException("Input is a base cell");
+        }
+        long parentH = H3Index.H3_set_resolution(h3, childRes - 1);
+        return H3Index.H3_set_index_digit(parentH, childRes, H3Index.H3_DIGIT_MASK);
+    }
+
+    /**
+     * Returns the parent of the given index.
+     */
+    public static String h3ToParent(String h3Address) {
+        long parent = h3ToParent(stringToH3(h3Address));
+        return h3ToString(parent);
+    }
+
+    /**
+     * Returns the children of the given index.
+     */
+    public static long[] h3ToChildren(long h3) {
+        long[] children = new long[cellToChildrenSize(h3)];
+        int res = H3Index.H3_get_resolution(h3);
+        Iterator.IterCellsChildren it = Iterator.iterInitParent(h3, res + 1);
+        int pos = 0;
+        while (it.h != Iterator.H3_NULL) {
+            children[pos++] = it.h;
+            Iterator.iterStepChild(it);
+        }
+        return children;
+    }
+
+    /**
+     * Transforms a list of H3 indexes in long form to a list of H3
+     * indexes in string form.
+     */
+    public static String[] h3ToChildren(String h3Address) {
+        return h3ToStringList(h3ToChildren(stringToH3(h3Address)));
+    }
+
+    public static String[] hexRing(String h3Address) {
+        return h3ToStringList(hexRing(stringToH3(h3Address)));
+    }
+
+    /**
+     * Returns the neighbor indexes.
+     *
+     * @param h3 Origin index
+     * @return All neighbor indexes from the origin
+     */
+    public static long[] hexRing(long h3) {
+        return HexRing.hexRing(h3);
+    }
+
+    /**
+     * cellToChildrenSize returns the exact number of children for a cell at a
+     * given child resolution.
+     *
+     * @param h         H3Index to find the number of children of
+     *
+     * @return int      Exact number of children (handles hexagons and pentagons
+     *                  correctly)
+     */
+    private static int cellToChildrenSize(long h) {
+        int n = 1;
+        if (H3Index.H3_is_pentagon(h)) {
+            return (1 + 5 * (_ipow(7, n) - 1) / 6);
+        } else {
+            return _ipow(7, n);
+        }
+    }
+
+    /**
+     * _ipow does integer exponentiation efficiently. Taken from StackOverflow.
+     *
+     * @param base the integer base (can be positive or negative)
+     * @param exp the integer exponent (should be nonnegative)
+     *
+     * @return the exponentiated value
+     */
+    private static int _ipow(int base, int exp) {
+        int result = 1;
+        while (exp != 0) {
+            if ((exp & 1) != 0) {
+                result *= base;
+            }
+            exp >>= 1;
+            base *= base;
+        }
+
+        return result;
+    }
+
+    private static String[] h3ToStringList(long[] h3s) {
+        return Arrays.stream(h3s).mapToObj(H3::h3ToString).toArray(String[]::new);
+    }
+
+    /**
+     * @throws IllegalArgumentException <code>res</code> is not a valid H3 resolution.
+     */
+    private static void checkResolution(int res) {
+        if (res < 0 || res > Constants.MAX_H3_RES) {
+            throw new IllegalArgumentException("resolution [" + res + "]  is out of range (must be 0 <= res <= 15)");
+        }
+    }
+}

+ 333 - 0
libs/h3/src/main/java/org/elasticsearch/h3/H3Index.java

@@ -0,0 +1,333 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.
+ *
+ * Copyright 2016-2018, 2020 Uber Technologies, Inc.
+ */
+package org.elasticsearch.h3;
+
+/**
+ * Functions that can be applied to an H3 index.
+ */
+final class H3Index {
+
+    /**
+     * Gets the integer base cell of h3.
+     */
+    public static int H3_get_base_cell(long h3) {
+        return ((int) ((((h3) & H3_BC_MASK) >> H3_BC_OFFSET)));
+    }
+
+    /**
+     * Returns <code>true</code> if this index is one of twelve pentagons per resolution.
+     */
+    public static boolean H3_is_pentagon(long h3) {
+        return BaseCells.isBaseCellPentagon(H3Index.H3_get_base_cell(h3)) && H3Index.h3LeadingNonZeroDigit(h3) == 0;
+    }
+
+    public static long H3_INIT = 35184372088831L;
+
+    /**
+     * The bit offset of the mode in an H3 index.
+     */
+    public static int H3_MODE_OFFSET = 59;
+
+    /**
+     * 1's in the 4 mode bits, 0's everywhere else.
+     */
+    public static long H3_MODE_MASK = 15L << H3_MODE_OFFSET;
+
+    /**
+     * 0's in the 4 mode bits, 1's everywhere else.
+     */
+    public static long H3_MODE_MASK_NEGATIVE = ~H3_MODE_MASK;
+
+    public static long H3_set_mode(long h3, long mode) {
+        return (h3 & H3_MODE_MASK_NEGATIVE) | (mode << H3_MODE_OFFSET);
+    }
+
+    /**
+     * The bit offset of the base cell in an H3 index.
+     */
+    public static int H3_BC_OFFSET = 45;
+    /**
+     * 1's in the 7 base cell bits, 0's everywhere else.
+     */
+    public static long H3_BC_MASK = 127L << H3_BC_OFFSET;
+
+    /**
+     * 0's in the 7 base cell bits, 1's everywhere else.
+     */
+    public static long H3_BC_MASK_NEGATIVE = ~H3_BC_MASK;
+
+    /**
+     * Sets the integer base cell of h3 to bc.
+     */
+    public static long H3_set_base_cell(long h3, long bc) {
+        return (h3 & H3_BC_MASK_NEGATIVE) | (bc << H3_BC_OFFSET);
+    }
+
+    public static int H3_RES_OFFSET = 52;
+    /**
+     * 1's in the 4 resolution bits, 0's everywhere else.
+     */
+    public static long H3_RES_MASK = 15L << H3_RES_OFFSET;
+
+    /**
+     * 0's in the 4 resolution bits, 1's everywhere else.
+     */
+    public static long H3_RES_MASK_NEGATIVE = ~H3_RES_MASK;
+
+    /**
+     * The bit offset of the max resolution digit in an H3 index.
+     */
+    public static int H3_MAX_OFFSET = 63;
+
+    /**
+     * 1 in the highest bit, 0's everywhere else.
+     */
+    public static long H3_HIGH_BIT_MASK = (1L << H3_MAX_OFFSET);
+
+    /**
+     * Gets the highest bit of the H3 index.
+     */
+    public static int H3_get_high_bit(long h3) {
+        return ((int) ((((h3) & H3_HIGH_BIT_MASK) >> H3_MAX_OFFSET)));
+    }
+
+    /**
+     * Sets the long resolution of h3.
+     */
+    public static long H3_set_resolution(long h3, long res) {
+        return (((h3) & H3_RES_MASK_NEGATIVE) | (((res)) << H3_RES_OFFSET));
+    }
+
+    /**
+     * The bit offset of the reserved bits in an H3 index.
+     */
+    public static int H3_RESERVED_OFFSET = 56;
+
+    /**
+     * 1's in the 3 reserved bits, 0's everywhere else.
+     */
+    public static long H3_RESERVED_MASK = (7L << H3_RESERVED_OFFSET);
+
+    /**
+     * Gets a value in the reserved space. Should always be zero for valid indexes.
+     */
+    public static int H3_get_reserved_bits(long h3) {
+        return ((int) ((((h3) & H3_RESERVED_MASK) >> H3_RESERVED_OFFSET)));
+    }
+
+    public static int H3_get_mode(long h3) {
+        return ((int) ((((h3) & H3_MODE_MASK) >> H3_MODE_OFFSET)));
+    }
+
+    /**
+     * Gets the integer resolution of h3.
+     */
+    public static int H3_get_resolution(long h3) {
+        return (int) ((h3 & H3_RES_MASK) >> H3_RES_OFFSET);
+    }
+
+    /**
+     * The number of bits in a single H3 resolution digit.
+     */
+    public static int H3_PER_DIGIT_OFFSET = 3;
+
+    /**
+     * 1's in the 3 bits of res 15 digit bits, 0's everywhere else.
+     */
+    public static long H3_DIGIT_MASK = 7L;
+
+    /**
+     * Gets the resolution res integer digit (0-7) of h3.
+     */
+    public static int H3_get_index_digit(long h3, int res) {
+        return ((int) ((((h3) >> ((Constants.MAX_H3_RES - (res)) * H3_PER_DIGIT_OFFSET)) & H3_DIGIT_MASK)));
+    }
+
+    /**
+     * Sets the resolution res digit of h3 to the integer digit (0-7)
+     */
+    public static long H3_set_index_digit(long h3, int res, long digit) {
+        int x = (Constants.MAX_H3_RES - res) * H3_PER_DIGIT_OFFSET;
+        return (((h3) & ~((H3_DIGIT_MASK << (x)))) | (((digit)) << x));
+    }
+
+    /**
+     * Returns whether or not a resolution is a Class III grid. Note that odd
+     * resolutions are Class III and even resolutions are Class II.
+     * @param res The H3 resolution.
+     * @return 1 if the resolution is a Class III grid, and 0 if the resolution is
+     *         a Class II grid.
+     */
+    public static boolean isResolutionClassIII(int res) {
+        return res % 2 != 0;
+    }
+
+    /**
+     * Convert an H3Index to a FaceIJK address.
+     * @param h3 The H3Index.
+     */
+    public static FaceIJK h3ToFaceIjk(long h3) {
+        int baseCell = H3Index.H3_get_base_cell(h3);
+        if (baseCell < 0 || baseCell >= Constants.NUM_BASE_CELLS) {  // LCOV_EXCL_BR_LINE
+            // Base cells less than zero can not be represented in an index
+            // To prevent reading uninitialized memory, we zero the output.
+            throw new IllegalArgumentException();
+        }
+        // adjust for the pentagonal missing sequence; all of sub-sequence 5 needs
+        // to be adjusted (and some of sub-sequence 4 below)
+        if (BaseCells.isBaseCellPentagon(baseCell) && h3LeadingNonZeroDigit(h3) == 5) {
+            h3 = h3Rotate60cw(h3);
+        }
+
+        // start with the "home" face and ijk+ coordinates for the base cell of c
+        FaceIJK fijk = BaseCells.getBaseFaceIJK(baseCell);
+        if (h3ToFaceIjkWithInitializedFijk(h3, fijk) == false) {
+            return fijk;  // no overage is possible; h lies on this face
+        }
+        // if we're here we have the potential for an "overage"; i.e., it is
+        // possible that c lies on an adjacent face
+
+        CoordIJK origIJK = new CoordIJK(fijk.coord.i, fijk.coord.j, fijk.coord.k);
+
+        // if we're in Class III, drop into the next finer Class II grid
+        int res = H3Index.H3_get_resolution(h3);
+        if (isResolutionClassIII(res)) {
+            // Class III
+            fijk.coord.downAp7r();
+            res++;
+        }
+
+        // adjust for overage if needed
+        // a pentagon base cell with a leading 4 digit requires special handling
+        boolean pentLeading4 = (BaseCells.isBaseCellPentagon(baseCell) && h3LeadingNonZeroDigit(h3) == 4);
+        if (fijk.adjustOverageClassII(res, pentLeading4, false) != FaceIJK.Overage.NO_OVERAGE) {
+            // if the base cell is a pentagon we have the potential for secondary
+            // overages
+            if (BaseCells.isBaseCellPentagon(baseCell)) {
+                FaceIJK.Overage overage;
+                do {
+                    overage = fijk.adjustOverageClassII(res, false, false);
+                } while (overage != FaceIJK.Overage.NO_OVERAGE);
+            }
+
+            if (res != H3Index.H3_get_resolution(h3)) {
+                fijk.coord.upAp7r();
+            }
+        } else if (res != H3Index.H3_get_resolution(h3)) {
+            fijk.coord = origIJK;
+        }
+        return fijk;
+    }
+
+    /**
+     * Returns the highest resolution non-zero digit in an H3Index.
+     * @param h The H3Index.
+     * @return The highest resolution non-zero digit in the H3Index.
+     */
+    public static int h3LeadingNonZeroDigit(long h) {
+        for (int r = 1; r <= H3Index.H3_get_resolution(h); r++) {
+            final int dir = H3Index.H3_get_index_digit(h, r);
+            if (dir != CoordIJK.Direction.CENTER_DIGIT.digit()) {
+                return dir;
+            }
+        }
+        // if we're here it's all 0's
+        return CoordIJK.Direction.CENTER_DIGIT.digit();
+    }
+
+    /**
+     * Convert an H3Index to the FaceIJK address on a specified icosahedral face.
+     * @param h The H3Index.
+     * @param fijk The FaceIJK address, initialized with the desired face
+     *        and normalized base cell coordinates.
+     * @return Returns true if the possibility of overage exists, otherwise false.
+     */
+    private static boolean h3ToFaceIjkWithInitializedFijk(long h, FaceIJK fijk) {
+
+        final int res = H3Index.H3_get_resolution(h);
+
+        // center base cell hierarchy is entirely on this face
+        final boolean possibleOverage = BaseCells.isBaseCellPentagon(H3_get_base_cell(h)) != false
+            || (res != 0 && (fijk.coord.i != 0 || fijk.coord.j != 0 || fijk.coord.k != 0));
+
+        for (int r = 1; r <= res; r++) {
+            if (isResolutionClassIII(r)) {
+                // Class III == rotate ccw
+                fijk.coord.downAp7();
+            } else {
+                // Class II == rotate cw
+                fijk.coord.downAp7r();
+            }
+            fijk.coord.neighbor(H3_get_index_digit(h, r));
+        }
+
+        return possibleOverage;
+    }
+
+    /**
+     * Rotate an H3Index 60 degrees clockwise.
+     * @param h The H3Index.
+     */
+    public static long h3Rotate60cw(long h) {
+        for (int r = 1, res = H3_get_resolution(h); r <= res; r++) {
+            h = H3_set_index_digit(h, r, CoordIJK.rotate60cw(H3_get_index_digit(h, r)));
+        }
+        return h;
+    }
+
+    /**
+     * Rotate an H3Index 60 degrees counter-clockwise.
+     * @param h The H3Index.
+     */
+    public static long h3Rotate60ccw(long h) {
+        for (int r = 1, res = H3_get_resolution(h); r <= res; r++) {
+            h = H3_set_index_digit(h, r, CoordIJK.rotate60ccw(H3_get_index_digit(h, r)));
+        }
+        return h;
+    }
+
+    /**
+     * Rotate an H3Index 60 degrees counter-clockwise about a pentagonal center.
+     * @param h The H3Index.
+     */
+    public static long h3RotatePent60ccw(long h) {
+        // skips any leading 1 digits (k-axis)
+        boolean foundFirstNonZeroDigit = false;
+        for (int r = 1, res = H3_get_resolution(h); r <= res; r++) {
+            // rotate this digit
+            h = H3_set_index_digit(h, r, CoordIJK.rotate60ccw(H3_get_index_digit(h, r)));
+
+            // look for the first non-zero digit so we
+            // can adjust for deleted k-axes sequence
+            // if necessary
+            if (foundFirstNonZeroDigit == false && H3_get_index_digit(h, r) != 0) {
+                foundFirstNonZeroDigit = true;
+
+                // adjust for deleted k-axes sequence
+                if (h3LeadingNonZeroDigit(h) == CoordIJK.Direction.K_AXES_DIGIT.digit()) h = h3Rotate60ccw(h);
+            }
+        }
+        return h;
+    }
+
+}

+ 756 - 0
libs/h3/src/main/java/org/elasticsearch/h3/HexRing.java

@@ -0,0 +1,756 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.
+ *
+ * Copyright 2016-2021 Uber Technologies, Inc.
+ */
+package org.elasticsearch.h3;
+
+/**
+ * Computes the neighbour H3 index from a given index.
+ */
+final class HexRing {
+
+    private static final int INVALID_BASE_CELL = 127;
+
+    /** Neighboring base cell ID in each IJK direction.
+     *
+     * For each base cell, for each direction, the neighboring base
+     * cell ID is given. 127 indicates there is no neighbor in that direction.
+     */
+    private static final int[][] baseCellNeighbors = new int[][] {
+        { 0, 1, 5, 2, 4, 3, 8 },                          // base cell 0
+        { 1, 7, 6, 9, 0, 3, 2 },                          // base cell 1
+        { 2, 6, 10, 11, 0, 1, 5 },                        // base cell 2
+        { 3, 13, 1, 7, 4, 12, 0 },                        // base cell 3
+        { 4, INVALID_BASE_CELL, 15, 8, 3, 0, 12 },        // base cell 4 (pentagon)
+        { 5, 2, 18, 10, 8, 0, 16 },                       // base cell 5
+        { 6, 14, 11, 17, 1, 9, 2 },                       // base cell 6
+        { 7, 21, 9, 19, 3, 13, 1 },                       // base cell 7
+        { 8, 5, 22, 16, 4, 0, 15 },                       // base cell 8
+        { 9, 19, 14, 20, 1, 7, 6 },                       // base cell 9
+        { 10, 11, 24, 23, 5, 2, 18 },                     // base cell 10
+        { 11, 17, 23, 25, 2, 6, 10 },                     // base cell 11
+        { 12, 28, 13, 26, 4, 15, 3 },                     // base cell 12
+        { 13, 26, 21, 29, 3, 12, 7 },                     // base cell 13
+        { 14, INVALID_BASE_CELL, 17, 27, 9, 20, 6 },      // base cell 14 (pentagon)
+        { 15, 22, 28, 31, 4, 8, 12 },                     // base cell 15
+        { 16, 18, 33, 30, 8, 5, 22 },                     // base cell 16
+        { 17, 11, 14, 6, 35, 25, 27 },                    // base cell 17
+        { 18, 24, 30, 32, 5, 10, 16 },                    // base cell 18
+        { 19, 34, 20, 36, 7, 21, 9 },                     // base cell 19
+        { 20, 14, 19, 9, 40, 27, 36 },                    // base cell 20
+        { 21, 38, 19, 34, 13, 29, 7 },                    // base cell 21
+        { 22, 16, 41, 33, 15, 8, 31 },                    // base cell 22
+        { 23, 24, 11, 10, 39, 37, 25 },                   // base cell 23
+        { 24, INVALID_BASE_CELL, 32, 37, 10, 23, 18 },    // base cell 24 (pentagon)
+        { 25, 23, 17, 11, 45, 39, 35 },                   // base cell 25
+        { 26, 42, 29, 43, 12, 28, 13 },                   // base cell 26
+        { 27, 40, 35, 46, 14, 20, 17 },                   // base cell 27
+        { 28, 31, 42, 44, 12, 15, 26 },                   // base cell 28
+        { 29, 43, 38, 47, 13, 26, 21 },                   // base cell 29
+        { 30, 32, 48, 50, 16, 18, 33 },                   // base cell 30
+        { 31, 41, 44, 53, 15, 22, 28 },                   // base cell 31
+        { 32, 30, 24, 18, 52, 50, 37 },                   // base cell 32
+        { 33, 30, 49, 48, 22, 16, 41 },                   // base cell 33
+        { 34, 19, 38, 21, 54, 36, 51 },                   // base cell 34
+        { 35, 46, 45, 56, 17, 27, 25 },                   // base cell 35
+        { 36, 20, 34, 19, 55, 40, 54 },                   // base cell 36
+        { 37, 39, 52, 57, 24, 23, 32 },                   // base cell 37
+        { 38, INVALID_BASE_CELL, 34, 51, 29, 47, 21 },    // base cell 38 (pentagon)
+        { 39, 37, 25, 23, 59, 57, 45 },                   // base cell 39
+        { 40, 27, 36, 20, 60, 46, 55 },                   // base cell 40
+        { 41, 49, 53, 61, 22, 33, 31 },                   // base cell 41
+        { 42, 58, 43, 62, 28, 44, 26 },                   // base cell 42
+        { 43, 62, 47, 64, 26, 42, 29 },                   // base cell 43
+        { 44, 53, 58, 65, 28, 31, 42 },                   // base cell 44
+        { 45, 39, 35, 25, 63, 59, 56 },                   // base cell 45
+        { 46, 60, 56, 68, 27, 40, 35 },                   // base cell 46
+        { 47, 38, 43, 29, 69, 51, 64 },                   // base cell 47
+        { 48, 49, 30, 33, 67, 66, 50 },                   // base cell 48
+        { 49, INVALID_BASE_CELL, 61, 66, 33, 48, 41 },    // base cell 49 (pentagon)
+        { 50, 48, 32, 30, 70, 67, 52 },                   // base cell 50
+        { 51, 69, 54, 71, 38, 47, 34 },                   // base cell 51
+        { 52, 57, 70, 74, 32, 37, 50 },                   // base cell 52
+        { 53, 61, 65, 75, 31, 41, 44 },                   // base cell 53
+        { 54, 71, 55, 73, 34, 51, 36 },                   // base cell 54
+        { 55, 40, 54, 36, 72, 60, 73 },                   // base cell 55
+        { 56, 68, 63, 77, 35, 46, 45 },                   // base cell 56
+        { 57, 59, 74, 78, 37, 39, 52 },                   // base cell 57
+        { 58, INVALID_BASE_CELL, 62, 76, 44, 65, 42 },    // base cell 58 (pentagon)
+        { 59, 63, 78, 79, 39, 45, 57 },                   // base cell 59
+        { 60, 72, 68, 80, 40, 55, 46 },                   // base cell 60
+        { 61, 53, 49, 41, 81, 75, 66 },                   // base cell 61
+        { 62, 43, 58, 42, 82, 64, 76 },                   // base cell 62
+        { 63, INVALID_BASE_CELL, 56, 45, 79, 59, 77 },    // base cell 63 (pentagon)
+        { 64, 47, 62, 43, 84, 69, 82 },                   // base cell 64
+        { 65, 58, 53, 44, 86, 76, 75 },                   // base cell 65
+        { 66, 67, 81, 85, 49, 48, 61 },                   // base cell 66
+        { 67, 66, 50, 48, 87, 85, 70 },                   // base cell 67
+        { 68, 56, 60, 46, 90, 77, 80 },                   // base cell 68
+        { 69, 51, 64, 47, 89, 71, 84 },                   // base cell 69
+        { 70, 67, 52, 50, 83, 87, 74 },                   // base cell 70
+        { 71, 89, 73, 91, 51, 69, 54 },                   // base cell 71
+        { 72, INVALID_BASE_CELL, 73, 55, 80, 60, 88 },    // base cell 72 (pentagon)
+        { 73, 91, 72, 88, 54, 71, 55 },                   // base cell 73
+        { 74, 78, 83, 92, 52, 57, 70 },                   // base cell 74
+        { 75, 65, 61, 53, 94, 86, 81 },                   // base cell 75
+        { 76, 86, 82, 96, 58, 65, 62 },                   // base cell 76
+        { 77, 63, 68, 56, 93, 79, 90 },                   // base cell 77
+        { 78, 74, 59, 57, 95, 92, 79 },                   // base cell 78
+        { 79, 78, 63, 59, 93, 95, 77 },                   // base cell 79
+        { 80, 68, 72, 60, 99, 90, 88 },                   // base cell 80
+        { 81, 85, 94, 101, 61, 66, 75 },                  // base cell 81
+        { 82, 96, 84, 98, 62, 76, 64 },                   // base cell 82
+        { 83, INVALID_BASE_CELL, 74, 70, 100, 87, 92 },   // base cell 83 (pentagon)
+        { 84, 69, 82, 64, 97, 89, 98 },                   // base cell 84
+        { 85, 87, 101, 102, 66, 67, 81 },                 // base cell 85
+        { 86, 76, 75, 65, 104, 96, 94 },                  // base cell 86
+        { 87, 83, 102, 100, 67, 70, 85 },                 // base cell 87
+        { 88, 72, 91, 73, 99, 80, 105 },                  // base cell 88
+        { 89, 97, 91, 103, 69, 84, 71 },                  // base cell 89
+        { 90, 77, 80, 68, 106, 93, 99 },                  // base cell 90
+        { 91, 73, 89, 71, 105, 88, 103 },                 // base cell 91
+        { 92, 83, 78, 74, 108, 100, 95 },                 // base cell 92
+        { 93, 79, 90, 77, 109, 95, 106 },                 // base cell 93
+        { 94, 86, 81, 75, 107, 104, 101 },                // base cell 94
+        { 95, 92, 79, 78, 109, 108, 93 },                 // base cell 95
+        { 96, 104, 98, 110, 76, 86, 82 },                 // base cell 96
+        { 97, INVALID_BASE_CELL, 98, 84, 103, 89, 111 },  // base cell 97 (pentagon)
+        { 98, 110, 97, 111, 82, 96, 84 },                 // base cell 98
+        { 99, 80, 105, 88, 106, 90, 113 },                // base cell 99
+        { 100, 102, 83, 87, 108, 114, 92 },               // base cell 100
+        { 101, 102, 107, 112, 81, 85, 94 },               // base cell 101
+        { 102, 101, 87, 85, 114, 112, 100 },              // base cell 102
+        { 103, 91, 97, 89, 116, 105, 111 },               // base cell 103
+        { 104, 107, 110, 115, 86, 94, 96 },               // base cell 104
+        { 105, 88, 103, 91, 113, 99, 116 },               // base cell 105
+        { 106, 93, 99, 90, 117, 109, 113 },               // base cell 106
+        { 107, INVALID_BASE_CELL, 101, 94, 115, 104, 112 },                                // base cell 107 (pentagon)
+        { 108, 100, 95, 92, 118, 114, 109 },    // base cell 108
+        { 109, 108, 93, 95, 117, 118, 106 },    // base cell 109
+        { 110, 98, 104, 96, 119, 111, 115 },    // base cell 110
+        { 111, 97, 110, 98, 116, 103, 119 },    // base cell 111
+        { 112, 107, 102, 101, 120, 115, 114 },  // base cell 112
+        { 113, 99, 116, 105, 117, 106, 121 },   // base cell 113
+        { 114, 112, 100, 102, 118, 120, 108 },  // base cell 114
+        { 115, 110, 107, 104, 120, 119, 112 },  // base cell 115
+        { 116, 103, 119, 111, 113, 105, 121 },  // base cell 116
+        { 117, INVALID_BASE_CELL, 109, 118, 113, 121, 106 },                                // base cell 117 (pentagon)
+        { 118, 120, 108, 114, 117, 121, 109 },  // base cell 118
+        { 119, 111, 115, 110, 121, 116, 120 },  // base cell 119
+        { 120, 115, 114, 112, 121, 119, 118 },  // base cell 120
+        { 121, 116, 120, 119, 117, 113, 118 },  // base cell 121
+    };
+
+    /** @brief Neighboring base cell rotations in each IJK direction.
+     *
+     * For each base cell, for each direction, the number of 60 degree
+     * CCW rotations to the coordinate system of the neighbor is given.
+     * -1 indicates there is no neighbor in that direction.
+     */
+    private static final int[][] baseCellNeighbor60CCWRots = new int[][] {
+        { 0, 5, 0, 0, 1, 5, 1 },   // base cell 0
+        { 0, 0, 1, 0, 1, 0, 1 },   // base cell 1
+        { 0, 0, 0, 0, 0, 5, 0 },   // base cell 2
+        { 0, 5, 0, 0, 2, 5, 1 },   // base cell 3
+        { 0, -1, 1, 0, 3, 4, 2 },  // base cell 4 (pentagon)
+        { 0, 0, 1, 0, 1, 0, 1 },   // base cell 5
+        { 0, 0, 0, 3, 5, 5, 0 },   // base cell 6
+        { 0, 0, 0, 0, 0, 5, 0 },   // base cell 7
+        { 0, 5, 0, 0, 0, 5, 1 },   // base cell 8
+        { 0, 0, 1, 3, 0, 0, 1 },   // base cell 9
+        { 0, 0, 1, 3, 0, 0, 1 },   // base cell 10
+        { 0, 3, 3, 3, 0, 0, 0 },   // base cell 11
+        { 0, 5, 0, 0, 3, 5, 1 },   // base cell 12
+        { 0, 0, 1, 0, 1, 0, 1 },   // base cell 13
+        { 0, -1, 3, 0, 5, 2, 0 },  // base cell 14 (pentagon)
+        { 0, 5, 0, 0, 4, 5, 1 },   // base cell 15
+        { 0, 0, 0, 0, 0, 5, 0 },   // base cell 16
+        { 0, 3, 3, 3, 3, 0, 3 },   // base cell 17
+        { 0, 0, 0, 3, 5, 5, 0 },   // base cell 18
+        { 0, 3, 3, 3, 0, 0, 0 },   // base cell 19
+        { 0, 3, 3, 3, 0, 3, 0 },   // base cell 20
+        { 0, 0, 0, 3, 5, 5, 0 },   // base cell 21
+        { 0, 0, 1, 0, 1, 0, 1 },   // base cell 22
+        { 0, 3, 3, 3, 0, 3, 0 },   // base cell 23
+        { 0, -1, 3, 0, 5, 2, 0 },  // base cell 24 (pentagon)
+        { 0, 0, 0, 3, 0, 0, 3 },   // base cell 25
+        { 0, 0, 0, 0, 0, 5, 0 },   // base cell 26
+        { 0, 3, 0, 0, 0, 3, 3 },   // base cell 27
+        { 0, 0, 1, 0, 1, 0, 1 },   // base cell 28
+        { 0, 0, 1, 3, 0, 0, 1 },   // base cell 29
+        { 0, 3, 3, 3, 0, 0, 0 },   // base cell 30
+        { 0, 0, 0, 0, 0, 5, 0 },   // base cell 31
+        { 0, 3, 3, 3, 3, 0, 3 },   // base cell 32
+        { 0, 0, 1, 3, 0, 0, 1 },   // base cell 33
+        { 0, 3, 3, 3, 3, 0, 3 },   // base cell 34
+        { 0, 0, 3, 0, 3, 0, 3 },   // base cell 35
+        { 0, 0, 0, 3, 0, 0, 3 },   // base cell 36
+        { 0, 3, 0, 0, 0, 3, 3 },   // base cell 37
+        { 0, -1, 3, 0, 5, 2, 0 },  // base cell 38 (pentagon)
+        { 0, 3, 0, 0, 3, 3, 0 },   // base cell 39
+        { 0, 3, 0, 0, 3, 3, 0 },   // base cell 40
+        { 0, 0, 0, 3, 5, 5, 0 },   // base cell 41
+        { 0, 0, 0, 3, 5, 5, 0 },   // base cell 42
+        { 0, 3, 3, 3, 0, 0, 0 },   // base cell 43
+        { 0, 0, 1, 3, 0, 0, 1 },   // base cell 44
+        { 0, 0, 3, 0, 0, 3, 3 },   // base cell 45
+        { 0, 0, 0, 3, 0, 3, 0 },   // base cell 46
+        { 0, 3, 3, 3, 0, 3, 0 },   // base cell 47
+        { 0, 3, 3, 3, 0, 3, 0 },   // base cell 48
+        { 0, -1, 3, 0, 5, 2, 0 },  // base cell 49 (pentagon)
+        { 0, 0, 0, 3, 0, 0, 3 },   // base cell 50
+        { 0, 3, 0, 0, 0, 3, 3 },   // base cell 51
+        { 0, 0, 3, 0, 3, 0, 3 },   // base cell 52
+        { 0, 3, 3, 3, 0, 0, 0 },   // base cell 53
+        { 0, 0, 3, 0, 3, 0, 3 },   // base cell 54
+        { 0, 0, 3, 0, 0, 3, 3 },   // base cell 55
+        { 0, 3, 3, 3, 0, 0, 3 },   // base cell 56
+        { 0, 0, 0, 3, 0, 3, 0 },   // base cell 57
+        { 0, -1, 3, 0, 5, 2, 0 },  // base cell 58 (pentagon)
+        { 0, 3, 3, 3, 3, 3, 0 },   // base cell 59
+        { 0, 3, 3, 3, 3, 3, 0 },   // base cell 60
+        { 0, 3, 3, 3, 3, 0, 3 },   // base cell 61
+        { 0, 3, 3, 3, 3, 0, 3 },   // base cell 62
+        { 0, -1, 3, 0, 5, 2, 0 },  // base cell 63 (pentagon)
+        { 0, 0, 0, 3, 0, 0, 3 },   // base cell 64
+        { 0, 3, 3, 3, 0, 3, 0 },   // base cell 65
+        { 0, 3, 0, 0, 0, 3, 3 },   // base cell 66
+        { 0, 3, 0, 0, 3, 3, 0 },   // base cell 67
+        { 0, 3, 3, 3, 0, 0, 0 },   // base cell 68
+        { 0, 3, 0, 0, 3, 3, 0 },   // base cell 69
+        { 0, 0, 3, 0, 0, 3, 3 },   // base cell 70
+        { 0, 0, 0, 3, 0, 3, 0 },   // base cell 71
+        { 0, -1, 3, 0, 5, 2, 0 },  // base cell 72 (pentagon)
+        { 0, 3, 3, 3, 0, 0, 3 },   // base cell 73
+        { 0, 3, 3, 3, 0, 0, 3 },   // base cell 74
+        { 0, 0, 0, 3, 0, 0, 3 },   // base cell 75
+        { 0, 3, 0, 0, 0, 3, 3 },   // base cell 76
+        { 0, 0, 0, 3, 0, 5, 0 },   // base cell 77
+        { 0, 3, 3, 3, 0, 0, 0 },   // base cell 78
+        { 0, 0, 1, 3, 1, 0, 1 },   // base cell 79
+        { 0, 0, 1, 3, 1, 0, 1 },   // base cell 80
+        { 0, 0, 3, 0, 3, 0, 3 },   // base cell 81
+        { 0, 0, 3, 0, 3, 0, 3 },   // base cell 82
+        { 0, -1, 3, 0, 5, 2, 0 },  // base cell 83 (pentagon)
+        { 0, 0, 3, 0, 0, 3, 3 },   // base cell 84
+        { 0, 0, 0, 3, 0, 3, 0 },   // base cell 85
+        { 0, 3, 0, 0, 3, 3, 0 },   // base cell 86
+        { 0, 3, 3, 3, 3, 3, 0 },   // base cell 87
+        { 0, 0, 0, 3, 0, 5, 0 },   // base cell 88
+        { 0, 3, 3, 3, 3, 3, 0 },   // base cell 89
+        { 0, 0, 0, 0, 0, 0, 1 },   // base cell 90
+        { 0, 3, 3, 3, 0, 0, 0 },   // base cell 91
+        { 0, 0, 0, 3, 0, 5, 0 },   // base cell 92
+        { 0, 5, 0, 0, 5, 5, 0 },   // base cell 93
+        { 0, 0, 3, 0, 0, 3, 3 },   // base cell 94
+        { 0, 0, 0, 0, 0, 0, 1 },   // base cell 95
+        { 0, 0, 0, 3, 0, 3, 0 },   // base cell 96
+        { 0, -1, 3, 0, 5, 2, 0 },  // base cell 97 (pentagon)
+        { 0, 3, 3, 3, 0, 0, 3 },   // base cell 98
+        { 0, 5, 0, 0, 5, 5, 0 },   // base cell 99
+        { 0, 0, 1, 3, 1, 0, 1 },   // base cell 100
+        { 0, 3, 3, 3, 0, 0, 3 },   // base cell 101
+        { 0, 3, 3, 3, 0, 0, 0 },   // base cell 102
+        { 0, 0, 1, 3, 1, 0, 1 },   // base cell 103
+        { 0, 3, 3, 3, 3, 3, 0 },   // base cell 104
+        { 0, 0, 0, 0, 0, 0, 1 },   // base cell 105
+        { 0, 0, 1, 0, 3, 5, 1 },   // base cell 106
+        { 0, -1, 3, 0, 5, 2, 0 },  // base cell 107 (pentagon)
+        { 0, 5, 0, 0, 5, 5, 0 },   // base cell 108
+        { 0, 0, 1, 0, 4, 5, 1 },   // base cell 109
+        { 0, 3, 3, 3, 0, 0, 0 },   // base cell 110
+        { 0, 0, 0, 3, 0, 5, 0 },   // base cell 111
+        { 0, 0, 0, 3, 0, 5, 0 },   // base cell 112
+        { 0, 0, 1, 0, 2, 5, 1 },   // base cell 113
+        { 0, 0, 0, 0, 0, 0, 1 },   // base cell 114
+        { 0, 0, 1, 3, 1, 0, 1 },   // base cell 115
+        { 0, 5, 0, 0, 5, 5, 0 },   // base cell 116
+        { 0, -1, 1, 0, 3, 4, 2 },  // base cell 117 (pentagon)
+        { 0, 0, 1, 0, 0, 5, 1 },   // base cell 118
+        { 0, 0, 0, 0, 0, 0, 1 },   // base cell 119
+        { 0, 5, 0, 0, 5, 5, 0 },   // base cell 120
+        { 0, 0, 1, 0, 1, 5, 1 },   // base cell 121
+    };
+
+    private static final int E_SUCCESS = 0; // Success (no error)
+    private static final int E_PENTAGON = 9;  // Pentagon distortion was encountered which the algorithm
+    private static final int E_CELL_INVALID = 5; // `H3Index` cell argument was not valid
+    private static final int E_FAILED = 1;  // The operation failed but a more specific error is not available
+
+    /**
+     * Directions used for traversing a hexagonal ring counterclockwise around
+     * {1, 0, 0}
+     *
+     * <pre>
+     *      _
+     *    _/ \\_
+     *   / \\5/ \\
+     *   \\0/ \\4/
+     *   / \\_/ \\
+     *   \\1/ \\3/
+     *     \\2/
+     * </pre>
+     */
+    private static final CoordIJK.Direction[] DIRECTIONS = new CoordIJK.Direction[] {
+        CoordIJK.Direction.J_AXES_DIGIT,
+        CoordIJK.Direction.JK_AXES_DIGIT,
+        CoordIJK.Direction.K_AXES_DIGIT,
+        CoordIJK.Direction.IK_AXES_DIGIT,
+        CoordIJK.Direction.I_AXES_DIGIT,
+        CoordIJK.Direction.IJ_AXES_DIGIT };
+
+    /**
+     * New digit when traversing along class II grids.
+     *
+     * Current digit -> direction -> new digit.
+     */
+    private static final CoordIJK.Direction[][] NEW_DIGIT_II = new CoordIJK.Direction[][] {
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT },
+        {
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT },
+        {
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT },
+        {
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT },
+        {
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT },
+        {
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT },
+        {
+            CoordIJK.Direction.IJ_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT } };
+
+    /**
+     * New traversal direction when traversing along class II grids.
+     *
+     * Current digit -> direction -> new ap7 move (at coarser level).
+     */
+    private static final CoordIJK.Direction[][] NEW_ADJUSTMENT_II = new CoordIJK.Direction[][] {
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT },
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT },
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT },
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT },
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT },
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT },
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT } };
+
+    /**
+     * New traversal direction when traversing along class III grids.
+     *
+     * Current digit -> direction -> new ap7 move (at coarser level).
+     */
+    private static final CoordIJK.Direction[][] NEW_DIGIT_III = new CoordIJK.Direction[][] {
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT },
+        {
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT },
+        {
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT },
+        {
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT },
+        {
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT },
+        {
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT },
+        {
+            CoordIJK.Direction.IJ_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT } };
+
+    /**
+     * New traversal direction when traversing along class III grids.
+     *
+     * Current digit -> direction -> new ap7 move (at coarser level).
+     */
+    private static final CoordIJK.Direction[][] NEW_ADJUSTMENT_III = new CoordIJK.Direction[][] {
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT },
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT },
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT },
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.J_AXES_DIGIT,
+            CoordIJK.Direction.JK_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT },
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT },
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.K_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.IK_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT },
+        {
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.I_AXES_DIGIT,
+            CoordIJK.Direction.CENTER_DIGIT,
+            CoordIJK.Direction.IJ_AXES_DIGIT } };
+
+    /**
+     * Produce all neighboring cells. For Hexagons there will be 6 neighbors while
+     * for pentagon just 5.
+     * Output is placed in the provided array in no particular order.
+     *
+     * @param  origin   origin cell
+     */
+    public static long[] hexRing(long origin) {
+        final long[] out = H3Index.H3_is_pentagon(origin) ? new long[5] : new long[6];
+        int idx = 0;
+        long previous = -1;
+        for (int i = 0; i < 6; i++) {
+            int[] rotations = new int[] { 0 };
+            long[] nextNeighbor = new long[] { 0 };
+            int neighborResult = h3NeighborRotations(origin, DIRECTIONS[i].digit(), rotations, nextNeighbor);
+            if (neighborResult != E_PENTAGON) {
+                // E_PENTAGON is an expected case when trying to traverse off of
+                // pentagons.
+                if (neighborResult != E_SUCCESS) {
+                    throw new IllegalArgumentException();
+                }
+                if (previous != nextNeighbor[0]) {
+                    out[idx++] = nextNeighbor[0];
+                    previous = nextNeighbor[0];
+                }
+            }
+        }
+        assert idx == out.length;
+        return out;
+    }
+
+    /**
+     * Returns the hexagon index neighboring the origin, in the direction dir.
+     *
+     * Implementation note: The only reachable case where this returns 0 is if the
+     * origin is a pentagon and the translation is in the k direction. Thus,
+     * 0 can only be returned if origin is a pentagon.
+     *
+     * @param origin Origin index
+     * @param dir Direction to move in
+     * @param rotations Number of ccw rotations to perform to reorient the
+     *                  translation vector. Will be modified to the new number of
+     *                  rotations to perform (such as when crossing a face edge.)
+     * @param out H3Index of the specified neighbor if succesful
+     * @return E_SUCCESS on success
+     */
+    private static int h3NeighborRotations(long origin, int dir, int[] rotations, long[] out) {
+        long current = origin;
+
+        for (int i = 0; i < rotations[0]; i++) {
+            dir = CoordIJK.rotate60ccw(dir);
+        }
+
+        int newRotations = 0;
+        int oldBaseCell = H3Index.H3_get_base_cell(current);
+        if (oldBaseCell < 0 || oldBaseCell >= Constants.NUM_BASE_CELLS) {  // LCOV_EXCL_BR_LINE
+            // Base cells less than zero can not be represented in an index
+            return E_CELL_INVALID;
+        }
+        int oldLeadingDigit = H3Index.h3LeadingNonZeroDigit(current);
+
+        // Adjust the indexing digits and, if needed, the base cell.
+        int r = H3Index.H3_get_resolution(current) - 1;
+        while (true) {
+            if (r == -1) {
+                current = H3Index.H3_set_base_cell(current, baseCellNeighbors[oldBaseCell][dir]);
+                newRotations = baseCellNeighbor60CCWRots[oldBaseCell][dir];
+
+                if (H3Index.H3_get_base_cell(current) == INVALID_BASE_CELL) {
+                    // Adjust for the deleted k vertex at the base cell level.
+                    // This edge actually borders a different neighbor.
+                    current = H3Index.H3_set_base_cell(current, baseCellNeighbors[oldBaseCell][CoordIJK.Direction.IK_AXES_DIGIT.digit()]);
+                    newRotations = baseCellNeighbor60CCWRots[oldBaseCell][CoordIJK.Direction.IK_AXES_DIGIT.digit()];
+
+                    // perform the adjustment for the k-subsequence we're skipping
+                    // over.
+                    current = H3Index.h3Rotate60ccw(current);
+                    rotations[0] = rotations[0] + 1;
+                }
+
+                break;
+            } else {
+                int oldDigit = H3Index.H3_get_index_digit(current, r + 1);
+                int nextDir;
+                if (oldDigit == CoordIJK.Direction.INVALID_DIGIT.digit()) {
+                    // Only possible on invalid input
+                    return E_CELL_INVALID;
+                } else if (H3Index.isResolutionClassIII(r + 1)) {
+                    current = H3Index.H3_set_index_digit(current, r + 1, NEW_DIGIT_II[oldDigit][dir].digit());
+                    nextDir = NEW_ADJUSTMENT_II[oldDigit][dir].digit();
+                } else {
+                    current = H3Index.H3_set_index_digit(current, r + 1, NEW_DIGIT_III[oldDigit][dir].digit());
+                    nextDir = NEW_ADJUSTMENT_III[oldDigit][dir].digit();
+                }
+
+                if (nextDir != CoordIJK.Direction.CENTER_DIGIT.digit()) {
+                    dir = nextDir;
+                    r--;
+                } else {
+                    // No more adjustment to perform
+                    break;
+                }
+            }
+        }
+
+        int newBaseCell = H3Index.H3_get_base_cell(current);
+        if (BaseCells.isBaseCellPentagon(newBaseCell)) {
+            boolean alreadyAdjustedKSubsequence = false;
+
+            // force rotation out of missing k-axes sub-sequence
+            if (H3Index.h3LeadingNonZeroDigit(current) == CoordIJK.Direction.K_AXES_DIGIT.digit()) {
+                if (oldBaseCell != newBaseCell) {
+                    // in this case, we traversed into the deleted
+                    // k subsequence of a pentagon base cell.
+                    // We need to rotate out of that case depending
+                    // on how we got here.
+                    // check for a cw/ccw offset face; default is ccw
+
+                    if (BaseCells.baseCellIsCwOffset(newBaseCell, BaseCells.getBaseFaceIJK(oldBaseCell).face)) {
+                        current = H3Index.h3Rotate60cw(current);
+                    } else {
+                        // See cwOffsetPent in testGridDisk.c for why this is
+                        // unreachable.
+                        current = H3Index.h3Rotate60ccw(current);  // LCOV_EXCL_LINE
+                    }
+                    alreadyAdjustedKSubsequence = true;
+                } else {
+                    // In this case, we traversed into the deleted
+                    // k subsequence from within the same pentagon
+                    // base cell.
+                    if (oldLeadingDigit == CoordIJK.Direction.CENTER_DIGIT.digit()) {
+                        // Undefined: the k direction is deleted from here
+                        return E_PENTAGON;
+                    } else if (oldLeadingDigit == CoordIJK.Direction.JK_AXES_DIGIT.digit()) {
+                        // Rotate out of the deleted k subsequence
+                        // We also need an additional change to the direction we're
+                        // moving in
+                        current = H3Index.h3Rotate60ccw(current);
+                        rotations[0] = rotations[0] + 1;
+                    } else if (oldLeadingDigit == CoordIJK.Direction.IK_AXES_DIGIT.digit()) {
+                        // Rotate out of the deleted k subsequence
+                        // We also need an additional change to the direction we're
+                        // moving in
+                        current = H3Index.h3Rotate60cw(current);
+                        rotations[0] = rotations[0] + 5;
+                    } else {
+                        // Should never occur
+                        return E_FAILED;  // LCOV_EXCL_LINE
+                    }
+                }
+            }
+
+            for (int i = 0; i < newRotations; i++)
+                current = H3Index.h3RotatePent60ccw(current);
+
+            // Account for differing orientation of the base cells (this edge
+            // might not follow properties of some other edges.)
+            if (oldBaseCell != newBaseCell) {
+                if (BaseCells.isBaseCellPolarPentagon(newBaseCell)) {
+                    // 'polar' base cells behave differently because they have all
+                    // i neighbors.
+                    if (oldBaseCell != 118
+                        && oldBaseCell != 8
+                        && H3Index.h3LeadingNonZeroDigit(current) != CoordIJK.Direction.JK_AXES_DIGIT.digit()) {
+                        rotations[0] = rotations[0] + 1;
+                    }
+                } else if (H3Index.h3LeadingNonZeroDigit(current) == CoordIJK.Direction.IK_AXES_DIGIT.digit()
+                    && alreadyAdjustedKSubsequence == false) {
+                        // account for distortion introduced to the 5 neighbor by the
+                        // deleted k subsequence.
+                        rotations[0] = rotations[0] + 1;
+                    }
+            }
+        } else {
+            for (int i = 0; i < newRotations; i++)
+                current = H3Index.h3Rotate60ccw(current);
+        }
+
+        rotations[0] = (rotations[0] + newRotations) % 6;
+        out[0] = current;
+
+        return E_SUCCESS;
+    }
+
+}

+ 306 - 0
libs/h3/src/main/java/org/elasticsearch/h3/Iterator.java

@@ -0,0 +1,306 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.
+ *
+ * Copyright 2021 Uber Technologies, Inc.
+ */
+package org.elasticsearch.h3;
+
+/**
+ * Iterator structures and functions for the children of a cell.
+ */
+final class Iterator {
+    /**
+     * Invalid index used to indicate an error from latLngToCell and related
+     * functions or missing data in arrays of H3 indices. Analogous to NaN in
+     * floating point.
+     */
+    public static final long H3_NULL = 0;
+
+    /**
+     * The number of bits in a single H3 resolution digit.
+     */
+    private static final int H3_PER_DIGIT_OFFSET = 3;
+
+    /**
+     * IterCellsChildren: struct for iterating through the descendants of
+     * a given cell.
+     * <p>
+     * Constructors:
+     * <p>
+     * Initialize with either `iterInitParent` or `iterInitBaseCellNum`.
+     * `iterInitParent` sets up an iterator for all the children of a given
+     * parent cell at a given resolution.
+     * <p>
+     * `iterInitBaseCellNum` sets up an iterator for children cells, given
+     * a base cell number (0--121).
+     * <p>
+     * Iteration:
+     * <p>
+     * Step iterator with `iterStepChild`.
+     * During the lifetime of the `IterCellsChildren`, the current iterate
+     * is accessed via the `IterCellsChildren.h` member.
+     * When the iterator is exhausted or if there was an error in initialization,
+     * `IterCellsChildren.h` will be `H3_NULL` even after calling `iterStepChild`.
+     */
+    static class IterCellsChildren {
+        long h;
+        int _parentRes;  // parent resolution
+        int _skipDigit;  // this digit skips `1` for pentagons
+
+        IterCellsChildren(long h, int _parentRes, int _skipDigit) {
+            this.h = h;
+            this._parentRes = _parentRes;
+            this._skipDigit = _skipDigit;
+        }
+    }
+
+    /**
+     * Create a fully nulled-out child iterator for when an iterator is exhausted.
+     * This helps minimize the chance that a user will depend on the iterator
+     * internal state after it's exhausted, like the child resolution, for
+     * example.
+     */
+    private static IterCellsChildren nullIter() {
+        return new IterCellsChildren(H3_NULL, -1, -1);
+    }
+
+    /**
+     ## Logic for iterating through the children of a cell
+     We'll describe the logic for ....
+     - normal (non pentagon iteration)
+     - pentagon iteration. define "pentagon digit"
+     ### Cell Index Component Diagrams
+     The lower 56 bits of an H3 Cell Index describe the following index components:
+     - the cell resolution (4 bits)
+     - the base cell number (7 bits)
+     - the child cell digit for each resolution from 1 to 15 (3*15 = 45 bits)
+     These are the bits we'll be focused on when iterating through child cells.
+     To help describe the iteration logic, we'll use diagrams displaying the
+     (decimal) values for each component like:
+     child digit for resolution 2
+     /
+     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | ... |
+     |-----|-------------|---|---|---|---|---|---|-----|
+     |   9 |          17 | 5 | 3 | 0 | 6 | 2 | 1 | ... |
+     ### Iteration through children of a hexagon (but not a pentagon)
+     Iteration through the children of a *hexagon* (but not a pentagon)
+     simply involves iterating through all the children values (0--6)
+     for each child digit (up to the child's resolution).
+     For example, suppose a resolution 3 hexagon index has the following
+     components:
+     parent resolution
+     /
+     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | ... |
+     |-----|-------------|---|---|---|---|---|---|-----|
+     |   3 |          17 | 3 | 5 | 1 | 7 | 7 | 7 | ... |
+     The iteration through all children of resolution 6 would look like:
+     parent res  child res
+     /           /
+     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ... |
+     |-----|-------------|---|---|---|---|---|---|---|---|-----|
+     | 6   |          17 | 3 | 5 | 1 | 0 | 0 | 0 | 7 | 7 | ... |
+     | 6   |          17 | 3 | 5 | 1 | 0 | 0 | 1 | 7 | 7 | ... |
+     | ... |             |   |   |   |   |   |   |   |   |     |
+     | 6   |          17 | 3 | 5 | 1 | 0 | 0 | 6 | 7 | 7 | ... |
+     | 6   |          17 | 3 | 5 | 1 | 0 | 1 | 0 | 7 | 7 | ... |
+     | 6   |          17 | 3 | 5 | 1 | 0 | 1 | 1 | 7 | 7 | ... |
+     | ... |             |   |   |   |   |   |   |   |   |     |
+     | 6   |          17 | 3 | 5 | 1 | 6 | 6 | 6 | 7 | 7 | ... |
+     ### Step sequence on a *pentagon* cell
+     Pentagon cells have a base cell number (e.g., 97) corresponding to a
+     resolution 0 pentagon, and have all zeros from digit 1 to the digit
+     corresponding to the cell's resolution.
+     (We'll drop the ellipses from now on, knowing that digits should contain
+     7's beyond the cell resolution.)
+     parent res      child res
+     /               /
+     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
+     |-----|-------------|---|---|---|---|---|---|
+     |   6 |          97 | 0 | 0 | 0 | 0 | 0 | 0 |
+     Iteration through children of a *pentagon* is almost the same
+     as *hexagon* iteration, except that we skip the *first* 1 value
+     that appears in the "skip digit". This corresponds to the fact
+     that a pentagon only has 6 children, which are denoted with
+     the numbers {0,2,3,4,5,6}.
+     The skip digit starts at the child resolution position.
+     When iterating through children more than one resolution below
+     the parent, we move the skip digit to the left
+     (up to the next coarser resolution) each time we skip the 1 value
+     in that digit.
+     Iteration would start like:
+     parent res      child res
+     /               /
+     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
+     |-----|-------------|---|---|---|---|---|---|
+     |   6 |          97 | 0 | 0 | 0 | 0 | 0 | 0 |
+     \
+     skip digit
+     Noticing we skip the 1 value and move the skip digit,
+     the next iterate would be:
+     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
+     |-----|-------------|---|---|---|---|---|---|
+     |   6 |          97 | 0 | 0 | 0 | 0 | 0 | 2 |
+     \
+     skip digit
+     Iteration continues normally until we get to:
+     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
+     |-----|-------------|---|---|---|---|---|---|
+     |   6 |          97 | 0 | 0 | 0 | 0 | 0 | 6 |
+     \
+     skip digit
+     which is followed by (skipping the 1):
+     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
+     |-----|-------------|---|---|---|---|---|---|
+     |   6 |          97 | 0 | 0 | 0 | 0 | 2 | 0 |
+     \
+     skip digit
+     For the next iterate, we won't skip the `1` in the previous digit
+     because it is no longer the skip digit:
+     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
+     |-----|-------------|---|---|---|---|---|---|
+     |   6 |          97 | 0 | 0 | 0 | 0 | 2 | 1 |
+     \
+     skip digit
+     Iteration continues normally until we're right before the next skip
+     digit:
+     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
+     |-----|-------------|---|---|---|---|---|---|
+     |   6 |          97 | 0 | 0 | 0 | 0 | 6 | 6 |
+     \
+     skip digit
+     Which is followed by
+     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
+     |-----|-------------|---|---|---|---|---|---|
+     |   6 |          97 | 0 | 0 | 0 | 2 | 0 | 0 |
+     \
+     skip digit
+     and so on.
+     */
+
+    /**
+     * Initialize a IterCellsChildren struct representing the sequence giving
+     * the children of cell `h` at resolution `childRes`.
+     * <p>
+     * At any point in the iteration, starting once
+     * the struct is initialized, IterCellsChildren.h gives the current child.
+     * <p>
+     * Also, IterCellsChildren.h == H3_NULL when all the children have been iterated
+     * through, or if the input to `iterInitParent` was invalid.
+     */
+    public static IterCellsChildren iterInitParent(long h, int childRes) {
+
+        int parentRes = H3Index.H3_get_resolution(h);
+
+        if (childRes < parentRes || childRes > Constants.MAX_H3_RES || h == H3_NULL) {
+            return nullIter();
+        }
+
+        long newH = zeroIndexDigits(h, parentRes + 1, childRes);
+        newH = H3Index.H3_set_resolution(newH, childRes);
+
+        int _skipDigit;
+        if (H3Index.H3_is_pentagon(newH)) {
+            // The skip digit skips `1` for pentagons.
+            // The "_skipDigit" moves to the left as we count up from the
+            // child resolution to the parent resolution.
+            _skipDigit = childRes;
+        } else {
+            // if not a pentagon, we can ignore "skip digit" logic
+            _skipDigit = -1;
+        }
+
+        return new IterCellsChildren(newH, parentRes, _skipDigit);
+    }
+
+    /**
+     * Step a IterCellsChildren to the next child cell.
+     * When the iteration is over, IterCellsChildren.h will be H3_NULL.
+     * Handles iterating through hexagon and pentagon cells.
+     */
+    public static void iterStepChild(IterCellsChildren it) {
+        // once h == H3_NULL, the iterator returns an infinite sequence of H3_NULL
+        if (it.h == H3_NULL) return;
+
+        int childRes = H3Index.H3_get_resolution(it.h);
+
+        incrementResDigit(it, childRes);
+
+        for (int i = childRes; i >= it._parentRes; i--) {
+            if (i == it._parentRes) {
+                // if we're modifying the parent resolution digit, then we're done
+                // *it = _null_iter();
+                it.h = H3_NULL;
+                return;
+            }
+
+            // PENTAGON_SKIPPED_DIGIT == 1
+            if (i == it._skipDigit && getResDigit(it, i) == CoordIJK.Direction.PENTAGON_SKIPPED_DIGIT.digit()) {
+                // Then we are iterating through the children of a pentagon cell.
+                // All children of a pentagon have the property that the first
+                // nonzero digit between the parent and child resolutions is
+                // not 1.
+                // I.e., we never see a sequence like 00001.
+                // Thus, we skip the `1` in this digit.
+                incrementResDigit(it, i);
+                it._skipDigit -= 1;
+                return;
+            }
+
+            // INVALID_DIGIT == 7
+            if (getResDigit(it, i) == CoordIJK.Direction.INVALID_DIGIT.digit()) {
+                incrementResDigit(it, i);  // zeros out it[i] and increments it[i-1] by 1
+            } else {
+                break;
+            }
+        }
+    }
+
+    // extract the `res` digit (0--7) of the current cell
+    private static int getResDigit(IterCellsChildren it, int res) {
+        return H3Index.H3_get_index_digit(it.h, res);
+    }
+
+    /**
+     * Zero out index digits from start to end, inclusive.
+     * No-op if start > end.
+     */
+    private static long zeroIndexDigits(long h, int start, int end) {
+        if (start > end) {
+            return h;
+        }
+
+        long m = 0;
+
+        m = ~m;
+        m <<= H3_PER_DIGIT_OFFSET * (end - start + 1);
+        m = ~m;
+        m <<= H3_PER_DIGIT_OFFSET * (Constants.MAX_H3_RES - end);
+        m = ~m;
+
+        return h & m;
+    }
+
+    // increment the digit (0--7) at location `res`
+    private static void incrementResDigit(IterCellsChildren it, int res) {
+        long val = 1;
+        val <<= H3_PER_DIGIT_OFFSET * (Constants.MAX_H3_RES - res);
+        it.h += val;
+    }
+}

+ 121 - 0
libs/h3/src/main/java/org/elasticsearch/h3/LatLng.java

@@ -0,0 +1,121 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.
+ *
+ * Copyright 2016-2021 Uber Technologies, Inc.
+ */
+package org.elasticsearch.h3;
+
+/** pair of latitude/longitude */
+public final class LatLng {
+
+    // lat / lon in radians
+    private final double lon;
+    private final double lat;
+
+    LatLng(double lat, double lon) {
+        this.lon = lon;
+        this.lat = lat;
+    }
+
+    /** Returns latitude in radians */
+    public double getLatRad() {
+        return lat;
+    }
+
+    /** Returns longitude in radians */
+    public double getLonRad() {
+        return lon;
+    }
+
+    /** Returns latitude in degrees */
+    public double getLatDeg() {
+        return Math.toDegrees(getLatRad());
+    }
+
+    /** Returns longitude in degrees */
+    public double getLonDeg() {
+        return Math.toDegrees(getLonRad());
+    }
+
+    /**
+     * Encodes a coordinate on the sphere to the corresponding icosahedral face and
+     * containing 2D hex coordinates relative to that face center.
+     *
+     * @param res The desired H3 resolution for the encoding.
+     */
+    FaceIJK geoToFaceIJK(int res) {
+        Vec3d v3d = new Vec3d(this);
+
+        // determine the icosahedron face
+        int face = 0;
+        double sqd = v3d.pointSquareDist(Vec3d.faceCenterPoint[0]);
+        for (int i = 1; i < Vec3d.faceCenterPoint.length; i++) {
+            double sqdT = v3d.pointSquareDist(Vec3d.faceCenterPoint[i]);
+            if (sqdT < sqd) {
+                face = i;
+                sqd = sqdT;
+            }
+        }
+        // cos(r) = 1 - 2 * sin^2(r/2) = 1 - 2 * (sqd / 4) = 1 - sqd/2
+        double r = Math.acos(1 - sqd / 2);
+
+        if (r < Constants.EPSILON) {
+            return new FaceIJK(face, new Vec2d(0.0, 0.0).hex2dToCoordIJK());
+        }
+
+        // now have face and r, now find CCW theta from CII i-axis
+        double theta = Vec2d.posAngleRads(
+            Vec2d.faceAxesAzRadsCII[face][0] - Vec2d.posAngleRads(Vec2d.faceCenterGeo[face].geoAzimuthRads(this))
+        );
+
+        // adjust theta for Class III (odd resolutions)
+        if (H3Index.isResolutionClassIII(res)) {
+            theta = Vec2d.posAngleRads(theta - Constants.M_AP7_ROT_RADS);
+        }
+
+        // perform gnomonic scaling of r
+        r = Math.tan(r);
+
+        // scale for current resolution length u
+        r /= Constants.RES0_U_GNOMONIC;
+        for (int i = 0; i < res; i++) {
+            r *= Constants.M_SQRT7;
+        }
+
+        // we now have (r, theta) in hex2d with theta ccw from x-axes
+
+        // convert to local x,y
+        Vec2d vec2d = new Vec2d(r * Math.cos(theta), r * Math.sin(theta));
+        return new FaceIJK(face, vec2d.hex2dToCoordIJK());
+    }
+
+    /**
+     * Determines the azimuth to the provided LatLng in radians.
+     *
+     * @param p The spherical coordinates.
+     * @return The azimuth in radians.
+     */
+    private double geoAzimuthRads(LatLng p) {
+        return Math.atan2(
+            Math.cos(p.lat) * Math.sin(p.lon - lon),
+            Math.cos(lat) * Math.sin(p.lat) - Math.sin(lat) * Math.cos(p.lat) * Math.cos(p.lon - lon)
+        );
+    }
+}

+ 404 - 0
libs/h3/src/main/java/org/elasticsearch/h3/Vec2d.java

@@ -0,0 +1,404 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.
+ *
+ * Copyright 2016-2017 Uber Technologies, Inc.
+ */
+package org.elasticsearch.h3;
+
+import java.util.Objects;
+
+/**
+ *  2D floating-point vector
+ */
+final class Vec2d {
+
+    /** sin(60') */
+    private static final double M_SIN60 = Constants.M_SQRT3_2;
+
+    /**
+     * icosahedron face centers in lat/lng radians
+     */
+    public static final LatLng[] faceCenterGeo = new LatLng[] {
+        new LatLng(0.803582649718989942, 1.248397419617396099),    // face 0
+        new LatLng(1.307747883455638156, 2.536945009877921159),    // face 1
+        new LatLng(1.054751253523952054, -1.347517358900396623),   // face 2
+        new LatLng(0.600191595538186799, -0.450603909469755746),   // face 3
+        new LatLng(0.491715428198773866, 0.401988202911306943),    // face 4
+        new LatLng(0.172745327415618701, 1.678146885280433686),    // face 5
+        new LatLng(0.605929321571350690, 2.953923329812411617),    // face 6
+        new LatLng(0.427370518328979641, -1.888876200336285401),   // face 7
+        new LatLng(-0.079066118549212831, -0.733429513380867741),  // face 8
+        new LatLng(-0.230961644455383637, 0.506495587332349035),   // face 9
+        new LatLng(0.079066118549212831, 2.408163140208925497),    // face 10
+        new LatLng(0.230961644455383637, -2.635097066257444203),   // face 11
+        new LatLng(-0.172745327415618701, -1.463445768309359553),  // face 12
+        new LatLng(-0.605929321571350690, -0.187669323777381622),  // face 13
+        new LatLng(-0.427370518328979641, 1.252716453253507838),   // face 14
+        new LatLng(-0.600191595538186799, 2.690988744120037492),   // face 15
+        new LatLng(-0.491715428198773866, -2.739604450678486295),  // face 16
+        new LatLng(-0.803582649718989942, -1.893195233972397139),  // face 17
+        new LatLng(-1.307747883455638156, -0.604647643711872080),  // face 18
+        new LatLng(-1.054751253523952054, 1.794075294689396615),   // face 19
+    };
+
+    /**
+     * icosahedron face ijk axes as azimuth in radians from face center to
+     * vertex 0/1/2 respectively
+     */
+    public static final double[][] faceAxesAzRadsCII = new double[][] {
+        { 5.619958268523939882, 3.525563166130744542, 1.431168063737548730 },  // face 0
+        { 5.760339081714187279, 3.665943979320991689, 1.571548876927796127 },  // face 1
+        { 0.780213654393430055, 4.969003859179821079, 2.874608756786625655 },  // face 2
+        { 0.430469363979999913, 4.619259568766391033, 2.524864466373195467 },  // face 3
+        { 6.130269123335111400, 4.035874020941915804, 1.941478918548720291 },  // face 4
+        { 2.692877706530642877, 0.598482604137447119, 4.787272808923838195 },  // face 5
+        { 2.982963003477243874, 0.888567901084048369, 5.077358105870439581 },  // face 6
+        { 3.532912002790141181, 1.438516900396945656, 5.627307105183336758 },  // face 7
+        { 3.494305004259568154, 1.399909901866372864, 5.588700106652763840 },  // face 8
+        { 3.003214169499538391, 0.908819067106342928, 5.097609271892733906 },  // face 9
+        { 5.930472956509811562, 3.836077854116615875, 1.741682751723420374 },  // face 10
+        { 0.138378484090254847, 4.327168688876645809, 2.232773586483450311 },  // face 11
+        { 0.448714947059150361, 4.637505151845541521, 2.543110049452346120 },  // face 12
+        { 0.158629650112549365, 4.347419854898940135, 2.253024752505744869 },  // face 13
+        { 5.891865957979238535, 3.797470855586042958, 1.703075753192847583 },  // face 14
+        { 2.711123289609793325, 0.616728187216597771, 4.805518392002988683 },  // face 15
+        { 3.294508837434268316, 1.200113735041072948, 5.388903939827463911 },  // face 16
+        { 3.804819692245439833, 1.710424589852244509, 5.899214794638635174 },  // face 17
+        { 3.664438879055192436, 1.570043776661997111, 5.758833981448388027 },  // face 18
+        { 2.361378999196363184, 0.266983896803167583, 4.455774101589558636 },  // face 19
+    };
+
+    /**
+     * pi
+     */
+    private static double M_PI = 3.14159265358979323846;
+    /**
+     * pi / 2.0
+     */
+    private static double M_PI_2 = 1.5707963267948966;
+    /**
+     * 2.0 * PI
+     */
+    public static double M_2PI = 6.28318530717958647692528676655900576839433;
+
+    private final double x;  /// < x component
+    private final double y;  /// < y component
+
+    Vec2d(double x, double y) {
+        this.x = x;
+        this.y = y;
+    }
+
+    /**
+     * Determines the center point in spherical coordinates of a cell given by 2D
+     * hex coordinates on a particular icosahedral face.
+     *
+     * @param face      The icosahedral face upon which the 2D hex coordinate system is
+     *                  centered.
+     * @param res       The H3 resolution of the cell.
+     * @param substrate Indicates whether or not this grid is actually a substrate
+     *                  grid relative to the specified resolution.
+     */
+    public LatLng hex2dToGeo(int face, int res, boolean substrate) {
+        // calculate (r, theta) in hex2d
+        double r = v2dMag();
+
+        if (r < Constants.EPSILON) {
+            return faceCenterGeo[face];
+        }
+
+        double theta = Math.atan2(y, x);
+
+        // scale for current resolution length u
+        for (int i = 0; i < res; i++) {
+            r /= Constants.M_SQRT7;
+        }
+
+        // scale accordingly if this is a substrate grid
+        if (substrate) {
+            r /= 3.0;
+            if (H3Index.isResolutionClassIII(res)) {
+                r /= Constants.M_SQRT7;
+            }
+        }
+
+        r *= Constants.RES0_U_GNOMONIC;
+
+        // perform inverse gnomonic scaling of r
+        r = Math.atan(r);
+
+        // adjust theta for Class III
+        // if a substrate grid, then it's already been adjusted for Class III
+        if (substrate == false && H3Index.isResolutionClassIII(res)) theta = posAngleRads(theta + Constants.M_AP7_ROT_RADS);
+
+        // find theta as an azimuth
+        theta = posAngleRads(faceAxesAzRadsCII[face][0] - theta);
+
+        // now find the point at (r,theta) from the face center
+        return geoAzDistanceRads(faceCenterGeo[face], theta, r);
+    }
+
+    /**
+     * Determine the containing hex in ijk+ coordinates for a 2D cartesian
+     * coordinate vector (from DGGRID).
+     *
+     */
+    public CoordIJK hex2dToCoordIJK() {
+        double a1, a2;
+        double x1, x2;
+        int m1, m2;
+        double r1, r2;
+
+        // quantize into the ij system and then normalize
+        int k = 0;
+        int i;
+        int j;
+
+        a1 = Math.abs(x);
+        a2 = Math.abs(y);
+
+        // first do a reverse conversion
+        x2 = a2 / M_SIN60;
+        x1 = a1 + x2 / 2.0;
+
+        // check if we have the center of a hex
+        m1 = (int) x1;
+        m2 = (int) x2;
+
+        // otherwise round correctly
+        r1 = x1 - m1;
+        r2 = x2 - m2;
+
+        if (r1 < 0.5) {
+            if (r1 < 1.0 / 3.0) {
+                if (r2 < (1.0 + r1) / 2.0) {
+                    i = m1;
+                    j = m2;
+                } else {
+                    i = m1;
+                    j = m2 + 1;
+                }
+            } else {
+                if (r2 < (1.0 - r1)) {
+                    j = m2;
+                } else {
+                    j = m2 + 1;
+                }
+
+                if ((1.0 - r1) <= r2 && r2 < (2.0 * r1)) {
+                    i = m1 + 1;
+                } else {
+                    i = m1;
+                }
+            }
+        } else {
+            if (r1 < 2.0 / 3.0) {
+                if (r2 < (1.0 - r1)) {
+                    j = m2;
+                } else {
+                    j = m2 + 1;
+                }
+
+                if ((2.0 * r1 - 1.0) < r2 && r2 < (1.0 - r1)) {
+                    i = m1;
+                } else {
+                    i = m1 + 1;
+                }
+            } else {
+                if (r2 < (r1 / 2.0)) {
+                    i = m1 + 1;
+                    j = m2;
+                } else {
+                    i = m1 + 1;
+                    j = m2 + 1;
+                }
+            }
+        }
+
+        // now fold across the axes if necessary
+
+        if (x < 0.0) {
+            if ((j % 2) == 0)  // even
+            {
+                int axisi = j / 2;
+                int diff = i - axisi;
+                i = i - 2 * diff;
+            } else {
+                int axisi = (j + 1) / 2;
+                int diff = i - axisi;
+                i = i - (2 * diff + 1);
+            }
+        }
+
+        if (y < 0.0) {
+            i = i - (2 * j + 1) / 2;
+            j = -1 * j;
+        }
+        CoordIJK coordIJK = new CoordIJK(i, j, k);
+        coordIJK.ijkNormalize();
+        return coordIJK;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Vec2d vec2d = (Vec2d) o;
+        return Double.compare(vec2d.x, x) == 0 && Double.compare(vec2d.y, y) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(x, y);
+    }
+
+    /**
+     * Finds the intersection between two lines. Assumes that the lines intersect
+     * and that the intersection is not at an endpoint of either line.
+     *
+     * @param p0 The first endpoint of the first line.
+     * @param p1 The second endpoint of the first line.
+     * @param p2 The first endpoint of the second line.
+     * @param p3 The second endpoint of the second line.
+     */
+    public static Vec2d v2dIntersect(Vec2d p0, Vec2d p1, Vec2d p2, Vec2d p3) {
+        double[] s1 = new double[2], s2 = new double[2];
+        s1[0] = p1.x - p0.x;
+        s1[1] = p1.y - p0.y;
+        s2[0] = p3.x - p2.x;
+        s2[1] = p3.y - p2.y;
+
+        float t;
+        t = (float) ((s2[0] * (p0.y - p2.y) - s2[1] * (p0.x - p2.x)) / (-s2[0] * s1[1] + s1[0] * s2[1]));
+
+        return new Vec2d(p0.x + (t * s1[0]), p0.y + (t * s1[1]));
+    }
+
+    /**
+     * Calculates the magnitude of a 2D cartesian vector.
+     *
+     * @return The magnitude of the vector.
+     */
+    private double v2dMag() {
+        return Math.sqrt(x * x + y * y);
+    }
+
+    /**
+     * Normalizes radians to a value between 0.0 and two PI.
+     *
+     * @param rads The input radians value.
+     * @return The normalized radians value.
+     */
+    static double posAngleRads(double rads) {
+        double tmp = ((rads < 0.0) ? rads + M_2PI : rads);
+        if (rads >= M_2PI) tmp -= M_2PI;
+        return tmp;
+    }
+
+    /**
+     * Computes the point on the sphere a specified azimuth and distance from
+     * another point.
+     *
+     * @param p1       The first spherical coordinates.
+     * @param az       The desired azimuth from p1.
+     * @param distance The desired distance from p1, must be non-negative.
+     *                 p1.
+     */
+    private static LatLng geoAzDistanceRads(LatLng p1, double az, double distance) {
+        if (distance < Constants.EPSILON) {
+            return p1;
+        }
+
+        double sinlat, sinlng, coslng;
+
+        az = posAngleRads(az);
+
+        double lat, lon;
+
+        // check for due north/south azimuth
+        if (az < Constants.EPSILON || Math.abs(az - M_PI) < Constants.EPSILON) {
+            if (az < Constants.EPSILON) {// due north
+                lat = p1.getLatRad() + distance;
+            } else { // due south
+                lat = p1.getLatRad() - distance;
+            }
+            if (Math.abs(lat - M_PI_2) < Constants.EPSILON) { // north pole
+                lat = M_PI_2;
+                lon = 0.0;
+            } else if (Math.abs(lat + M_PI_2) < Constants.EPSILON) { // south pole
+                lat = -M_PI_2;
+                lon = 0.0;
+            } else {
+                lon = constrainLng(p1.getLonRad());
+            }
+        } else { // not due north or south
+            sinlat = Math.sin(p1.getLatRad()) * Math.cos(distance) + Math.cos(p1.getLatRad()) * Math.sin(distance) * Math.cos(az);
+            if (sinlat > 1.0) {
+                sinlat = 1.0;
+            }
+            if (sinlat < -1.0) {
+                sinlat = -1.0;
+            }
+            lat = Math.asin(sinlat);
+            if (Math.abs(lat - M_PI_2) < Constants.EPSILON)  // north pole
+            {
+                lat = M_PI_2;
+                lon = 0.0;
+            } else if (Math.abs(lat + M_PI_2) < Constants.EPSILON)  // south pole
+            {
+                lat = -M_PI_2;
+                lon = 0.0;
+            } else {
+                sinlng = Math.sin(az) * Math.sin(distance) / Math.cos(lat);
+                coslng = (Math.cos(distance) - Math.sin(p1.getLatRad()) * Math.sin(lat)) / Math.cos(p1.getLatRad()) / Math.cos(lat);
+                if (sinlng > 1.0) {
+                    sinlng = 1.0;
+                }
+                if (sinlng < -1.0) {
+                    sinlng = -1.0;
+                }
+                if (coslng > 1.0) {
+                    coslng = 1.0;
+                }
+                if (coslng < -1.0) {
+                    coslng = -1.0;
+                }
+                lon = constrainLng(p1.getLonRad() + Math.atan2(sinlng, coslng));
+            }
+        }
+        return new LatLng(lat, lon);
+    }
+
+    /**
+     * constrainLng makes sure longitudes are in the proper bounds
+     *
+     * @param lng The origin lng value
+     * @return The corrected lng value
+     */
+    private static double constrainLng(double lng) {
+        while (lng > M_PI) {
+            lng = lng - (2 * M_PI);
+        }
+        while (lng < -M_PI) {
+            lng = lng + (2 * M_PI);
+        }
+        return lng;
+    }
+}

+ 83 - 0
libs/h3/src/main/java/org/elasticsearch/h3/Vec3d.java

@@ -0,0 +1,83 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.
+ *
+ * Copyright 2018, 2020-2021 Uber Technologies, Inc.
+ */
+
+package org.elasticsearch.h3;
+
+final class Vec3d {
+
+    /** icosahedron face centers in x/y/z on the unit sphere */
+    public static final double[][] faceCenterPoint = new double[][] {
+        { 0.2199307791404606, 0.6583691780274996, 0.7198475378926182 },     // face 0
+        { -0.2139234834501421, 0.1478171829550703, 0.9656017935214205 },    // face 1
+        { 0.1092625278784797, -0.4811951572873210, 0.8697775121287253 },    // face 2
+        { 0.7428567301586791, -0.3593941678278028, 0.5648005936517033 },    // face 3
+        { 0.8112534709140969, 0.3448953237639384, 0.4721387736413930 },     // face 4
+        { -0.1055498149613921, 0.9794457296411413, 0.1718874610009365 },    // face 5
+        { -0.8075407579970092, 0.1533552485898818, 0.5695261994882688 },    // face 6
+        { -0.2846148069787907, -0.8644080972654206, 0.4144792552473539 },   // face 7
+        { 0.7405621473854482, -0.6673299564565524, -0.0789837646326737 },   // face 8
+        { 0.8512303986474293, 0.4722343788582681, -0.2289137388687808 },    // face 9
+        { -0.7405621473854481, 0.6673299564565524, 0.0789837646326737 },    // face 10
+        { -0.8512303986474292, -0.4722343788582682, 0.2289137388687808 },   // face 11
+        { 0.1055498149613919, -0.9794457296411413, -0.1718874610009365 },   // face 12
+        { 0.8075407579970092, -0.1533552485898819, -0.5695261994882688 },   // face 13
+        { 0.2846148069787908, 0.8644080972654204, -0.4144792552473539 },    // face 14
+        { -0.7428567301586791, 0.3593941678278027, -0.5648005936517033 },   // face 15
+        { -0.8112534709140971, -0.3448953237639382, -0.4721387736413930 },  // face 16
+        { -0.2199307791404607, -0.6583691780274996, -0.7198475378926182 },  // face 17
+        { 0.2139234834501420, -0.1478171829550704, -0.9656017935214205 },   // face 18
+        { -0.1092625278784796, 0.4811951572873210, -0.8697775121287253 },   // face 19
+    };
+
+    private final double x;
+    private final double y;
+    private final double z;
+
+    Vec3d(LatLng latLng) {
+        double r = Math.cos(latLng.getLatRad());
+        this.z = Math.sin(latLng.getLatRad());
+        this.x = Math.cos(latLng.getLonRad()) * r;
+        this.y = Math.sin(latLng.getLonRad()) * r;
+    }
+
+    /**
+     * Calculate the square of the distance between two 3D coordinates.
+     *
+     * @param v The first 3D coordinate.
+     * @return The square of the distance between the given points.
+     */
+    public double pointSquareDist(double[] v) {
+        return square(x - v[0]) + square(y - v[1]) + square(z - v[2]);
+    }
+
+    /**
+     * Square of a number
+     *
+     * @param x The input number.
+     * @return The square of the input number.
+     */
+    private double square(double x) {
+        return x * x;
+    }
+
+}

+ 178 - 0
libs/h3/src/test/java/org/elasticsearch/h3/CellBoundaryTests.java

@@ -0,0 +1,178 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.elasticsearch.h3;
+
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+import org.elasticsearch.test.ESTestCase;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+public class CellBoundaryTests extends ESTestCase {
+
+    public void testRes0() throws Exception {
+        processFile("res00cells.txt");
+    }
+
+    public void testRes1() throws Exception {
+        processFile("res01cells.txt");
+    }
+
+    public void testRes2() throws Exception {
+        processFile("res02cells.txt");
+    }
+
+    public void testRes3() throws Exception {
+        processFile("res03cells.txt");
+    }
+
+    public void testBc05r08cells() throws Exception {
+        processFile("bc05r08cells.txt");
+    }
+
+    public void testBc05r09cells() throws Exception {
+        processFile("bc05r09cells.txt");
+    }
+
+    public void testBc05r10cells() throws Exception {
+        processFile("bc05r10cells.txt");
+    }
+
+    public void testBc05r11cells() throws Exception {
+        processFile("bc05r11cells.txt");
+    }
+
+    public void testBc05r12cells() throws Exception {
+        processFile("bc05r12cells.txt");
+    }
+
+    public void testBc05r13cells() throws Exception {
+        processFile("bc05r13cells.txt");
+    }
+
+    public void testBc05r05cells() throws Exception {
+        processFile("bc05r14cells.txt");
+    }
+
+    public void testBc05r15cells() throws Exception {
+        processFile("bc05r15cells.txt");
+    }
+
+    public void testBc14r08cells() throws Exception {
+        processFile("bc14r08cells.txt");
+    }
+
+    public void testBc14r09cells() throws Exception {
+        processFile("bc14r09cells.txt");
+    }
+
+    public void testBc14r10cells() throws Exception {
+        processFile("bc14r10cells.txt");
+    }
+
+    public void testBc14r11cells() throws Exception {
+        processFile("bc14r11cells.txt");
+    }
+
+    public void testBc14r12cells() throws Exception {
+        processFile("bc14r12cells.txt");
+    }
+
+    public void testBc14r13cells() throws Exception {
+        processFile("bc14r13cells.txt");
+    }
+
+    public void testBc14r14cells() throws Exception {
+        processFile("bc14r14cells.txt");
+    }
+
+    public void testBc14r15cells() throws Exception {
+        processFile("bc14r15cells.txt");
+    }
+
+    public void testBc19r08cells() throws Exception {
+        processFile("bc19r08cells.txt");
+    }
+
+    public void testBc19r09cells() throws Exception {
+        processFile("bc19r09cells.txt");
+    }
+
+    public void testBc19r10cells() throws Exception {
+        processFile("bc19r10cells.txt");
+    }
+
+    public void testBc19r11cells() throws Exception {
+        processFile("bc19r11cells.txt");
+    }
+
+    public void testBc19r12cells() throws Exception {
+        processFile("bc19r12cells.txt");
+    }
+
+    public void testBc19r13cells() throws Exception {
+        processFile("bc19r13cells.txt");
+    }
+
+    public void testBc19r14cells() throws Exception {
+        processFile("bc19r14cells.txt");
+    }
+
+    private void processFile(String file) throws IOException {
+        InputStream fis = getClass().getResourceAsStream(file + ".gz");
+        BufferedReader reader = new BufferedReader(new InputStreamReader(new GzipCompressorInputStream(fis), StandardCharsets.UTF_8));
+        String h3Address = reader.readLine();
+        while (h3Address != null) {
+            assertEquals(true, H3.h3IsValid(h3Address));
+            long h3 = H3.stringToH3(h3Address);
+            assertEquals(true, H3.h3IsValid(h3));
+            processOne(h3Address, reader);
+            h3Address = reader.readLine();
+        }
+    }
+
+    private void processOne(String h3Address, BufferedReader reader) throws IOException {
+        String line = reader.readLine();
+        if ("{".equals(line) == false) {
+            throw new IllegalArgumentException();
+        }
+        line = reader.readLine();
+        List<double[]> points = new ArrayList<>();
+        while ("}".equals(line) == false) {
+            StringTokenizer tokens = new StringTokenizer(line, " ");
+            assertEquals(2, tokens.countTokens());
+            double lat = Double.parseDouble(tokens.nextToken());
+            double lon = Double.parseDouble(tokens.nextToken());
+            points.add(new double[] { lat, lon });
+            line = reader.readLine();
+        }
+        CellBoundary boundary = H3.h3ToGeoBoundary(h3Address);
+        assert boundary.numPoints() == points.size();
+        for (int i = 0; i < boundary.numPoints(); i++) {
+            assertEquals(h3Address, points.get(i)[0], boundary.getLatLon(i).getLatDeg(), 1e-8);
+            assertEquals(h3Address, points.get(i)[1], boundary.getLatLon(i).getLonDeg(), 1e-8);
+        }
+    }
+}

+ 183 - 0
libs/h3/src/test/java/org/elasticsearch/h3/CellCenterTests.java

@@ -0,0 +1,183 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.elasticsearch.h3;
+
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+import org.elasticsearch.test.ESTestCase;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.StringTokenizer;
+
+public class CellCenterTests extends ESTestCase {
+
+    public void testRes0() throws Exception {
+        processFile("res00ic.txt");
+    }
+
+    public void testRes1() throws Exception {
+        processFile("res01ic.txt");
+    }
+
+    public void testRes2() throws Exception {
+        processFile("res02ic.txt");
+    }
+
+    public void testRes3() throws Exception {
+        processFile("res03ic.txt");
+    }
+
+    public void testBc05r08centers() throws Exception {
+        processFile("bc05r08centers.txt");
+    }
+
+    public void testBc05r09centers() throws Exception {
+        processFile("bc05r09centers.txt");
+    }
+
+    public void testBc05r10centers() throws Exception {
+        processFile("bc05r10centers.txt");
+    }
+
+    public void testBc05r11centers() throws Exception {
+        processFile("bc05r11centers.txt");
+    }
+
+    public void testBc05r12centers() throws Exception {
+        processFile("bc05r12centers.txt");
+    }
+
+    public void testBc05r13centers() throws Exception {
+        processFile("bc05r13centers.txt");
+    }
+
+    public void testBc05r05centers() throws Exception {
+        processFile("bc05r14centers.txt");
+    }
+
+    public void testBc05r15centers() throws Exception {
+        processFile("bc05r15centers.txt");
+    }
+
+    public void testBc14r08centers() throws Exception {
+        processFile("bc14r08centers.txt");
+    }
+
+    public void testBc14r09centers() throws Exception {
+        processFile("bc14r09centers.txt");
+    }
+
+    public void testBc14r10centers() throws Exception {
+        processFile("bc14r10centers.txt");
+    }
+
+    public void testBc14r11centers() throws Exception {
+        processFile("bc14r11centers.txt");
+    }
+
+    public void testBc14r12centers() throws Exception {
+        processFile("bc14r12centers.txt");
+    }
+
+    public void testBc14r13centers() throws Exception {
+        processFile("bc14r13centers.txt");
+    }
+
+    public void testBc14r14centers() throws Exception {
+        processFile("bc14r14centers.txt");
+    }
+
+    public void testBc14r15centers() throws Exception {
+        processFile("bc14r15centers.txt");
+    }
+
+    public void testBc19r08centers() throws Exception {
+        processFile("bc19r08centers.txt");
+    }
+
+    public void testBc19r09centers() throws Exception {
+        processFile("bc19r09centers.txt");
+    }
+
+    public void testBc19r10centers() throws Exception {
+        processFile("bc19r10centers.txt");
+    }
+
+    public void testBc19r11centers() throws Exception {
+        processFile("bc19r11centers.txt");
+    }
+
+    public void testBc19r12centers() throws Exception {
+        processFile("bc19r12centers.txt");
+    }
+
+    public void testBc19r13centers() throws Exception {
+        processFile("bc19r13centers.txt");
+    }
+
+    public void testBc19r14centers() throws Exception {
+        processFile("bc19r14centers.txt");
+    }
+
+    public void testBc19r15centers() throws Exception {
+        processFile("bc19r15centers.txt");
+    }
+
+    private void processFile(String file) throws IOException {
+        InputStream fis = getClass().getResourceAsStream(file + ".gz");
+        BufferedReader reader = new BufferedReader(new InputStreamReader(new GzipCompressorInputStream(fis), StandardCharsets.UTF_8));
+        String line = reader.readLine();
+        while (line != null) {
+            StringTokenizer tokenizer = new StringTokenizer(line, " ");
+            assertEquals(3, tokenizer.countTokens());
+            String h3Address = tokenizer.nextToken();
+            assertEquals(h3Address, true, H3.h3IsValid(h3Address));
+            double lat = Double.parseDouble(tokenizer.nextToken());
+            double lon = Double.parseDouble(tokenizer.nextToken());
+            assertH3ToLatLng(h3Address, lat, lon);
+            assertGeoToH3(h3Address, lat, lon);
+            assertHexRing(h3Address);
+            line = reader.readLine();
+        }
+    }
+
+    private void assertH3ToLatLng(String h3Address, double lat, double lon) {
+        LatLng latLng = H3.h3ToLatLng(h3Address);
+        assertEquals(h3Address, lat, latLng.getLatDeg(), 1e-6);
+        assertEquals(h3Address, lon, latLng.getLonDeg(), 1e-6);
+    }
+
+    private void assertGeoToH3(String h3Address, double lat, double lon) {
+        String computedH3Address = H3.geoToH3Address(lat, lon, H3Index.H3_get_resolution(H3.stringToH3(h3Address)));
+        assertEquals(h3Address, computedH3Address);
+        assertEquals(h3Address, computedH3Address);
+    }
+
+    private void assertHexRing(String h3Address) {
+        String[] neighbors = H3.hexRing(h3Address);
+        long center = H3.stringToH3(h3Address);
+        for (String neighbor : neighbors) {
+            long l = H3.stringToH3(neighbor);
+            assertEquals(H3Index.H3_get_resolution(center), H3Index.H3_get_resolution(l));
+        }
+    }
+}

+ 56 - 0
libs/h3/src/test/java/org/elasticsearch/h3/GeoToH3Tests.java

@@ -0,0 +1,56 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.elasticsearch.h3;
+
+import org.apache.lucene.geo.GeoTestUtil;
+import org.elasticsearch.test.ESTestCase;
+
+public class GeoToH3Tests extends ESTestCase {
+
+    public void testRandomPoints() {
+        for (int i = 0; i < 50; i++) {
+            // avoid points close to the poles
+            double lat = randomValueOtherThanMany(d -> d > 60 || d < -60, GeoTestUtil::nextLatitude);
+            // avoid points close to the dateline
+            double lon = randomValueOtherThanMany(d -> d > 150 || d < -150, GeoTestUtil::nextLongitude);
+            testPoint(lat, lon);
+        }
+    }
+
+    private void testPoint(double lat, double lon) {
+        for (int i = 0; i < Constants.MAX_H3_RES; i++) {
+            String h3Address = H3.geoToH3Address(lat, lon, i);
+            CellBoundary cellBoundary = H3.h3ToGeoBoundary(h3Address);
+            double minLat = cellBoundary.getLatLon(0).getLatDeg();
+            double maxLat = cellBoundary.getLatLon(0).getLatDeg();
+            double minLon = cellBoundary.getLatLon(0).getLonDeg();
+            double maxLon = cellBoundary.getLatLon(0).getLonDeg();
+            for (int j = 0; j < cellBoundary.numPoints(); j++) {
+                minLat = Math.min(minLat, cellBoundary.getLatLon(j).getLatDeg());
+                maxLat = Math.max(maxLat, cellBoundary.getLatLon(j).getLatDeg());
+                minLon = Math.min(minLon, cellBoundary.getLatLon(j).getLonDeg());
+                maxLon = Math.max(maxLon, cellBoundary.getLatLon(j).getLonDeg());
+            }
+            assertTrue(minLat <= lat);
+            assertTrue(maxLat >= lat);
+            assertTrue(minLon <= lon);
+            assertTrue(maxLon >= lon);
+        }
+    }
+}

+ 68 - 0
libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java

@@ -0,0 +1,68 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.elasticsearch.h3;
+
+import com.carrotsearch.randomizedtesting.generators.RandomPicks;
+
+import org.elasticsearch.test.ESTestCase;
+
+public class ParentChildNavigationTests extends ESTestCase {
+
+    public void testParentChild() {
+        String[] h3Addresses = H3.getStringRes0Cells();
+        String h3Address = RandomPicks.randomFrom(random(), h3Addresses);
+        String[] values = new String[H3.MAX_H3_RES];
+        values[0] = h3Address;
+        for (int i = 1; i < H3.MAX_H3_RES; i++) {
+            h3Addresses = H3.h3ToChildren(h3Address);
+            h3Address = RandomPicks.randomFrom(random(), h3Addresses);
+            values[i] = h3Address;
+        }
+        h3Addresses = H3.h3ToChildren(h3Address);
+        h3Address = RandomPicks.randomFrom(random(), h3Addresses);
+        for (int i = H3.MAX_H3_RES - 1; i >= 0; i--) {
+            h3Address = H3.h3ToParent(h3Address);
+            assertEquals(values[i], h3Address);
+        }
+    }
+
+    public void testHexRing() {
+        String[] h3Addresses = H3.getStringRes0Cells();
+        String h3Address = RandomPicks.randomFrom(random(), h3Addresses);
+        for (int i = 1; i < H3.MAX_H3_RES; i++) {
+            h3Addresses = H3.h3ToChildren(h3Address);
+            assertHexRing(i, h3Address, h3Addresses);
+            h3Address = RandomPicks.randomFrom(random(), h3Addresses);
+        }
+    }
+
+    private static final int[] HEX_RING_POSITIONS = new int[] { 2, 0, 1, 4, 3, 5 };
+    private static final int[] PENT_RING_POSITIONS = new int[] { 0, 1, 3, 2, 4 };
+
+    private void assertHexRing(int res, String h3Address, String[] children) {
+        LatLng latLng = H3.h3ToLatLng(h3Address);
+        String centerChild = H3.geoToH3Address(latLng.getLatDeg(), latLng.getLonDeg(), res);
+        assertEquals(children[0], centerChild);
+        String[] ring = H3.hexRing(centerChild);
+        int[] positions = H3.isPentagon(centerChild) ? PENT_RING_POSITIONS : HEX_RING_POSITIONS;
+        for (int i = 1; i < children.length; i++) {
+            assertEquals(children[i], ring[positions[i - 1]]);
+        }
+    }
+}

+ 4 - 0
libs/h3/src/test/resources/org/elasticsearch/h3/NOTICE.txt

@@ -0,0 +1,4 @@
+The files under this directory come from the input test files from Uber's h3 repository
+(https://github.com/uber/h3/tree/master/tests/inputfiles) and are made available here
+under the same Apache 2 license.
+

BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r08cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r08centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r09cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r09centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r10cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r10centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r11cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r11centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r12cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r12centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r13cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r13centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r14cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r14centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r15cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc05r15centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r08cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r08centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r09cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r09centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r10cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r10centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r11cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r11centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r12cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r12centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r13cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r13centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r14cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r14centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r15cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc14r15centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r08cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r08centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r09cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r09centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r10cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r10centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r11cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r11centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r12cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r12centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r13cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r13centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r14cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r14centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r15cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/bc19r15centers.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/res00cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/res00ic.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/res01cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/res01ic.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/res02cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/res02ic.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/res03cells.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/res03ic.txt.gz


BIN
libs/h3/src/test/resources/org/elasticsearch/h3/res04ic.txt.gz