You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucenenet.apache.org by ni...@apache.org on 2016/11/25 11:07:20 UTC

[15/26] lucenenet git commit: Added missing Spatial classes, ported Spatial tests, and fixed several bugs

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/d8c73530/src/Lucene.Net.Tests.Spatial/Lucene.Net.Tests.Spatial.csproj
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Tests.Spatial/Lucene.Net.Tests.Spatial.csproj b/src/Lucene.Net.Tests.Spatial/Lucene.Net.Tests.Spatial.csproj
new file mode 100644
index 0000000..d74a255
--- /dev/null
+++ b/src/Lucene.Net.Tests.Spatial/Lucene.Net.Tests.Spatial.csproj
@@ -0,0 +1,130 @@
+\ufeff<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{31F52F5C-A08F-4363-8003-23D6F7D6EB3A}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>Lucene.Net.Tests.Spatial</RootNamespace>
+    <AssemblyName>Lucene.Net.Tests.Spatial</AssemblyName>
+    <TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="nunit.framework, Version=2.6.3.13283, Culture=neutral, PublicKeyToken=96d09a1eb7f44a77, processorArchitecture=MSIL">
+      <HintPath>..\..\packages\NUnit.2.6.3\lib\nunit.framework.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
+    <Reference Include="Spatial4n.Core.NTS">
+      <HintPath>..\..\packages\Spatial4n.Core.NTS.0.4.1\lib\net40\Spatial4n.Core.NTS.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
+    <Reference Include="GeoAPI, Version=1.7.4.0, Culture=neutral, PublicKeyToken=a1a0da7def465678, processorArchitecture=MSIL">
+      <HintPath>..\..\packages\GeoAPI.1.7.4\lib\net45\GeoAPI.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
+    <Reference Include="NetTopologySuite, Version=1.14.0.0, Culture=neutral, PublicKeyToken=f580a05016ebada1, processorArchitecture=MSIL">
+      <HintPath>..\..\packages\NetTopologySuite.1.14\lib\net45\NetTopologySuite.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
+    <Reference Include="PowerCollections, Version=1.0.0.0, Culture=neutral, PublicKeyToken=2573bf8a1bdddcd5, processorArchitecture=MSIL">
+      <HintPath>..\..\packages\NetTopologySuite.1.14\lib\net45\PowerCollections.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="DistanceStrategyTest.cs" />
+    <Compile Include="PortedSolr3Test.cs" />
+    <Compile Include="Prefix\NtsPolygonTest.cs" />
+    <Compile Include="Prefix\SpatialOpRecursivePrefixTreeTest.cs" />
+    <Compile Include="Prefix\TestRecursivePrefixTreeStrategy.cs" />
+    <Compile Include="Prefix\TestTermQueryPrefixGridStrategy.cs" />
+    <Compile Include="Prefix\Tree\SpatialPrefixTreeTest.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="QueryEqualsHashCodeTest.cs" />
+    <Compile Include="Query\SpatialArgsParserTest.cs" />
+    <Compile Include="Serialized\SerializedStrategyTest.cs" />
+    <Compile Include="SpatialArgsTest.cs" />
+    <Compile Include="SpatialExample.cs" />
+    <Compile Include="SpatialMatchConcern.cs" />
+    <Compile Include="SpatialTestCase.cs" />
+    <Compile Include="SpatialTestData.cs" />
+    <Compile Include="SpatialTestQuery.cs" />
+    <Compile Include="StrategyTestCase.cs" />
+    <Compile Include="TestTestFramework.cs" />
+    <Compile Include="Vector\TestPointVectorStrategy.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\Lucene.Net.Core\Lucene.Net.csproj">
+      <Project>{5d4ad9be-1ffb-41ab-9943-25737971bf57}</Project>
+      <Name>Lucene.Net</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\Lucene.Net.Queries\Lucene.Net.Queries.csproj">
+      <Project>{69D7956C-C2CC-4708-B399-A188FEC384C4}</Project>
+      <Name>Lucene.Net.Queries</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\Lucene.Net.Spatial\Lucene.Net.Spatial.csproj">
+      <Project>{35c347f4-24b2-4be5-8117-a0e3001551ce}</Project>
+      <Name>Lucene.Net.Spatial</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\Lucene.Net.TestFramework\Lucene.Net.TestFramework.csproj">
+      <Project>{B2C0D749-CE34-4F62-A15E-00CB2FF5DDB3}</Project>
+      <Name>Lucene.Net.TestFramework</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
+  <ItemGroup>
+    <Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
+  </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="Test-Files\cities-Intersects-BBox.txt" />
+    <EmbeddedResource Include="Test-Files\Data\countries-bbox.txt" />
+    <EmbeddedResource Include="Test-Files\Data\countries-poly.txt" />
+    <EmbeddedResource Include="Test-Files\Data\geonames-IE.txt" />
+    <EmbeddedResource Include="Test-Files\Data\LUCENE-4464.txt" />
+    <EmbeddedResource Include="Test-Files\Data\simple-bbox.txt" />
+    <EmbeddedResource Include="Test-Files\Data\states-bbox.txt" />
+    <EmbeddedResource Include="Test-Files\Data\states-poly.txt" />
+    <EmbeddedResource Include="Test-Files\Data\world-cities-points.txt" />
+    <EmbeddedResource Include="Test-Files\simple-Queries-BBox.txt" />
+    <EmbeddedResource Include="Test-Files\states-Intersects-BBox.txt" />
+    <EmbeddedResource Include="Test-Files\states-IsWithin-BBox.txt" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/d8c73530/src/Lucene.Net.Tests.Spatial/PortedSolr3Test.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Tests.Spatial/PortedSolr3Test.cs b/src/Lucene.Net.Tests.Spatial/PortedSolr3Test.cs
new file mode 100644
index 0000000..e120b0b
--- /dev/null
+++ b/src/Lucene.Net.Tests.Spatial/PortedSolr3Test.cs
@@ -0,0 +1,201 @@
+\ufeffusing Lucene.Net.Randomized.Generators;
+using Lucene.Net.Search;
+using Lucene.Net.Spatial.Prefix;
+using Lucene.Net.Spatial.Prefix.Tree;
+using Lucene.Net.Spatial.Queries;
+using Lucene.Net.Spatial.Vector;
+using NUnit.Framework;
+using Spatial4n.Core.Context;
+using Spatial4n.Core.Distance;
+using Spatial4n.Core.Shapes;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace Lucene.Net.Spatial
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF 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.
+     */
+
+    /// <summary>
+    /// Based off of Solr 3's SpatialFilterTest.
+    /// </summary>
+    public class PortedSolr3Test : StrategyTestCase
+    {
+        //@ParametersFactory
+        public static IList<Object[]> Parameters()
+        {
+            List<Object[]> ctorArgs = new List<object[]>();
+
+            SpatialContext ctx = SpatialContext.GEO;
+            SpatialPrefixTree grid;
+            SpatialStrategy strategy;
+
+            grid = new GeohashPrefixTree(ctx, 12);
+            strategy = new RecursivePrefixTreeStrategy(grid, "recursive_geohash");
+            ctorArgs.Add(new Object[] { new Param(strategy) });
+
+            grid = new QuadPrefixTree(ctx, 25);
+            strategy = new RecursivePrefixTreeStrategy(grid, "recursive_quad");
+            ctorArgs.Add(new Object[] { new Param(strategy) });
+
+            grid = new GeohashPrefixTree(ctx, 12);
+            strategy = new TermQueryPrefixTreeStrategy(grid, "termquery_geohash");
+            ctorArgs.Add(new Object[] { new Param(strategy) });
+
+            strategy = new PointVectorStrategy(ctx, "pointvector");
+            ctorArgs.Add(new Object[] { new Param(strategy) });
+
+            return ctorArgs;
+        }
+
+        // this is a hack for clover! (otherwise strategy.toString() used as file name)
+        public class Param
+        {
+            internal SpatialStrategy strategy;
+
+            internal Param(SpatialStrategy strategy) { this.strategy = strategy; }
+
+
+            public override String ToString() { return strategy.FieldName; }
+        }
+
+        //  private String fieldName;
+
+        //public PortedSolr3Test(Param param)
+        //{
+        //    SpatialStrategy strategy = param.strategy;
+        //    this.ctx = strategy.SpatialContext;
+        //    this.strategy = strategy;
+        //}
+
+        public override void SetUp()
+        {
+            base.SetUp();
+            SpatialStrategy strategy = ((Param)(RandomInts.RandomFrom(Random(), Parameters()))[0]).strategy;
+            this.ctx = strategy.SpatialContext;
+            this.strategy = strategy;
+        }
+
+        private void setupDocs()
+        {
+            base.DeleteAll();
+            adoc("1", ctx.MakePoint(-79.9289094, 32.7693246));
+            adoc("2", ctx.MakePoint(-80.9289094, 33.7693246));
+            adoc("3", ctx.MakePoint(50.9289094, -32.7693246));
+            adoc("4", ctx.MakePoint(60.9289094, -50.7693246));
+            adoc("5", ctx.MakePoint(0, 0));
+            adoc("6", ctx.MakePoint(0.1, 0.1));
+            adoc("7", ctx.MakePoint(-0.1, -0.1));
+            adoc("8", ctx.MakePoint(179.9, 0));
+            adoc("9", ctx.MakePoint(-179.9, 0));
+            adoc("10", ctx.MakePoint(50, 89.9));
+            adoc("11", ctx.MakePoint(-130, 89.9));
+            adoc("12", ctx.MakePoint(50, -89.9));
+            adoc("13", ctx.MakePoint(-130, -89.9));
+            Commit();
+        }
+
+
+        [Test]
+        public virtual void TestIntersections()
+        {
+            setupDocs();
+            //Try some edge cases
+            //NOTE: 2nd arg is distance in kilometers
+            CheckHitsCircle(ctx.MakePoint(1, 1), 175, 3, 5, 6, 7);
+            CheckHitsCircle(ctx.MakePoint(179.8, 0), 200, 2, 8, 9);
+            CheckHitsCircle(ctx.MakePoint(50, 89.8), 200, 2, 10, 11);//this goes over the north pole
+            CheckHitsCircle(ctx.MakePoint(50, -89.8), 200, 2, 12, 13);//this goes over the south pole
+                                                                      //try some normal cases
+            CheckHitsCircle(ctx.MakePoint(-80.0, 33.0), 300, 2);
+            //large distance
+            CheckHitsCircle(ctx.MakePoint(1, 1), 5000, 3, 5, 6, 7);
+            //Because we are generating a box based on the west/east longitudes and the south/north latitudes, which then
+            //translates to a range query, which is slightly more inclusive.  Thus, even though 0.0 is 15.725 kms away,
+            //it will be included, b/c of the box calculation.
+            CheckHitsBBox(ctx.MakePoint(0.1, 0.1), 15, 2, 5, 6);
+            //try some more
+            DeleteAll();
+            adoc("14", ctx.MakePoint(5, 0));
+            adoc("15", ctx.MakePoint(15, 0));
+            //3000KM from 0,0, see http://www.movable-type.co.uk/scripts/latlong.html
+            adoc("16", ctx.MakePoint(19.79750, 18.71111));
+            adoc("17", ctx.MakePoint(-95.436643, 44.043900));
+            Commit();
+
+            CheckHitsCircle(ctx.MakePoint(0, 0), 1000, 1, 14);
+            CheckHitsCircle(ctx.MakePoint(0, 0), 2000, 2, 14, 15);
+            CheckHitsBBox(ctx.MakePoint(0, 0), 3000, 3, 14, 15, 16);
+            CheckHitsCircle(ctx.MakePoint(0, 0), 3001, 3, 14, 15, 16);
+            CheckHitsCircle(ctx.MakePoint(0, 0), 3000.1, 3, 14, 15, 16);
+
+            //really fine grained distance and reflects some of the vagaries of how we are calculating the box
+            CheckHitsCircle(ctx.MakePoint(-96.789603, 43.517030), 109, 0);
+
+            // falls outside of the real distance, but inside the bounding box
+            CheckHitsCircle(ctx.MakePoint(-96.789603, 43.517030), 110, 0);
+            CheckHitsBBox(ctx.MakePoint(-96.789603, 43.517030), 110, 1, 17);
+        }
+
+        //---- these are similar to Solr test methods
+
+        private void CheckHitsCircle(IPoint pt, double distKM, int assertNumFound, params int[] assertIds)
+        {
+            _CheckHits(false, pt, distKM, assertNumFound, assertIds);
+        }
+        private void CheckHitsBBox(IPoint pt, double distKM, int assertNumFound, params int[] assertIds)
+        {
+            _CheckHits(true, pt, distKM, assertNumFound, assertIds);
+        }
+
+        private void _CheckHits(bool bbox, IPoint pt, double distKM, int assertNumFound, params int[] assertIds)
+        {
+            SpatialOperation op = SpatialOperation.Intersects;
+            double distDEG = DistanceUtils.Dist2Degrees(distKM, DistanceUtils.EARTH_MEAN_RADIUS_KM);
+            IShape shape = ctx.MakeCircle(pt, distDEG);
+            if (bbox)
+                shape = shape.BoundingBox;
+
+            SpatialArgs args = new SpatialArgs(op, shape);
+            //args.setDistPrecision(0.025);
+            Query query;
+            if (Random().nextBoolean())
+            {
+                query = strategy.MakeQuery(args);
+            }
+            else
+            {
+                query = new FilteredQuery(new MatchAllDocsQuery(), strategy.MakeFilter(args));
+            }
+            SearchResults results = executeQuery(query, 100);
+            assertEquals("" + shape, assertNumFound, results.numFound);
+            if (assertIds != null)
+            {
+                ISet<int?> resultIds = new HashSet<int?>();
+                foreach (SearchResult result in results.results)
+                {
+                    resultIds.add(int.Parse(result.document.Get("id"), CultureInfo.InvariantCulture));
+                }
+                foreach (int assertId in assertIds)
+                {
+                    assertTrue("has " + assertId, resultIds.contains(assertId));
+                }
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/d8c73530/src/Lucene.Net.Tests.Spatial/Prefix/NtsPolygonTest.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Tests.Spatial/Prefix/NtsPolygonTest.cs b/src/Lucene.Net.Tests.Spatial/Prefix/NtsPolygonTest.cs
new file mode 100644
index 0000000..7f4338d
--- /dev/null
+++ b/src/Lucene.Net.Tests.Spatial/Prefix/NtsPolygonTest.cs
@@ -0,0 +1,118 @@
+\ufeffusing Lucene.Net.Documents;
+using Lucene.Net.Search;
+using Lucene.Net.Spatial.Prefix.Tree;
+using Lucene.Net.Spatial.Queries;
+using Lucene.Net.Support;
+using NUnit.Framework;
+using Spatial4n.Core.Context;
+using Spatial4n.Core.Context.Nts;
+using Spatial4n.Core.Shapes;
+using System;
+using System.Collections.Generic;
+
+namespace Lucene.Net.Spatial.Prefix
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF 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.
+     */
+
+    public class NtsPolygonTest : StrategyTestCase
+    {
+        private static readonly double LUCENE_4464_distErrPct = SpatialArgs.DEFAULT_DISTERRPCT;//DEFAULT 2.5%
+
+        public NtsPolygonTest()
+        {
+            try
+            {
+                IDictionary<string, string> args = new Dictionary<string, string>();
+                args.Put("spatialContextFactory",
+                    typeof(NtsSpatialContextFactory).AssemblyQualifiedName);
+                ctx = SpatialContextFactory.MakeSpatialContext(args /*, getClass().getClassLoader()*/);
+            }
+            catch (TypeLoadException e) //LUCENENET TODO: Does this match NoClassDefFoundError ??
+            {
+                AssumeTrue("This test requires JTS jar: " + e, false);
+            }
+
+            GeohashPrefixTree grid = new GeohashPrefixTree(ctx, 11);//< 1 meter == 11 maxLevels
+            this.strategy = new RecursivePrefixTreeStrategy(grid, GetType().Name);
+            ((RecursivePrefixTreeStrategy)this.strategy).DistErrPct = (LUCENE_4464_distErrPct);//1% radius (small!)
+        }
+
+        [Test]
+        /** LUCENE-4464 */
+        public virtual void TestCloseButNoMatch()
+        {
+            getAddAndVerifyIndexedDocuments("LUCENE-4464.txt");
+            SpatialArgs args = q(
+                "POLYGON((-93.18100824442227 45.25676372469945," +
+                    "-93.23182001200654 45.21421290799412," +
+                    "-93.16315546122038 45.23742639412364," +
+                    "-93.18100824442227 45.25676372469945))",
+                LUCENE_4464_distErrPct);
+            SearchResults got = executeQuery(strategy.MakeQuery(args), 100);
+            assertEquals(1, got.numFound);
+            assertEquals("poly2", got.results[0].document.Get("id"));
+            //did not find poly 1 !
+        }
+
+        private SpatialArgs q(String shapeStr, double distErrPct)
+        {
+            IShape shape = ctx.ReadShapeFromWkt(shapeStr);
+            SpatialArgs args = new SpatialArgs(SpatialOperation.Intersects, shape);
+            args.DistErrPct = (distErrPct);
+            return args;
+        }
+
+        /**
+         * A PrefixTree pruning optimization gone bad.
+         * See <a href="https://issues.apache.org/jira/browse/LUCENE-4770>LUCENE-4770</a>.
+         */
+        [Test]
+        public virtual void TestBadPrefixTreePrune()
+        {
+
+            IShape area = ctx.ReadShapeFromWkt("POLYGON((-122.83 48.57, -122.77 48.56, -122.79 48.53, -122.83 48.57))");
+
+            SpatialPrefixTree trie = new QuadPrefixTree(ctx, 12);
+            TermQueryPrefixTreeStrategy strategy = new TermQueryPrefixTreeStrategy(trie, "geo");
+            Document doc = new Document();
+            doc.Add(new TextField("id", "1", Field.Store.YES)); // LUCENENET TODO: Change API to Store.Yes
+
+            Field[] fields = strategy.CreateIndexableFields(area, 0.025);
+            foreach (Field field in fields)
+            {
+                doc.Add(field);
+            }
+            AddDocument(doc);
+
+            IPoint upperleft = ctx.MakePoint(-122.88, 48.54);
+            IPoint lowerright = ctx.MakePoint(-122.82, 48.62);
+
+            Query query = strategy.MakeQuery(new SpatialArgs(SpatialOperation.Intersects, ctx.MakeRectangle(upperleft, lowerright)));
+            Commit();
+
+            TopDocs search = indexSearcher.Search(query, 10);
+            ScoreDoc[] scoreDocs = search.ScoreDocs;
+            foreach (ScoreDoc scoreDoc in scoreDocs)
+            {
+                Console.WriteLine(indexSearcher.Doc(scoreDoc.Doc));
+            }
+
+            assertEquals(1, search.TotalHits);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/d8c73530/src/Lucene.Net.Tests.Spatial/Prefix/SpatialOpRecursivePrefixTreeTest.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Tests.Spatial/Prefix/SpatialOpRecursivePrefixTreeTest.cs b/src/Lucene.Net.Tests.Spatial/Prefix/SpatialOpRecursivePrefixTreeTest.cs
new file mode 100644
index 0000000..d7d3fb7
--- /dev/null
+++ b/src/Lucene.Net.Tests.Spatial/Prefix/SpatialOpRecursivePrefixTreeTest.cs
@@ -0,0 +1,485 @@
+\ufeffusing Lucene.Net.Documents;
+using Lucene.Net.Search;
+using Lucene.Net.Spatial.Prefix.Tree;
+using Lucene.Net.Spatial.Queries;
+using Lucene.Net.Support;
+using NUnit.Framework;
+using Spatial4n.Core.Context;
+using Spatial4n.Core.Shapes;
+using Spatial4n.Core.Shapes.Impl;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Lucene.Net.Spatial.Prefix
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF 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.
+     */
+
+    public class SpatialOpRecursivePrefixTreeTest : StrategyTestCase
+    {
+        const int ITERATIONS = 1;//Test Iterations
+
+        private SpatialPrefixTree grid;
+
+
+        public override void SetUp()
+        {
+            base.SetUp();
+            DeleteAll();
+        }
+
+        public virtual void SetupGrid(int maxLevels)
+        {
+            if (Random().nextBoolean())
+                SetupQuadGrid(maxLevels);
+            else
+                SetupGeohashGrid(maxLevels);
+            //((PrefixTreeStrategy) strategy).setDistErrPct(0);//fully precise to grid
+
+            Console.WriteLine("Strategy: " + strategy.toString());
+        }
+
+        private void SetupQuadGrid(int maxLevels)
+        {
+            //non-geospatial makes this test a little easier (in gridSnap), and using boundary values 2^X raises
+            // the prospect of edge conditions we want to test, plus makes for simpler numbers (no decimals).
+            SpatialContextFactory factory = new SpatialContextFactory();
+            factory.geo = false;
+            factory.worldBounds = new Rectangle(0, 256, -128, 128, null);
+            this.ctx = factory.NewSpatialContext();
+            //A fairly shallow grid, and default 2.5% distErrPct
+            if (maxLevels == -1)
+                maxLevels = randomIntBetween(1, 8);//max 64k cells (4^8), also 256*256
+            this.grid = new QuadPrefixTree(ctx, maxLevels);
+            this.strategy = new RecursivePrefixTreeStrategy(grid, GetType().Name);
+        }
+
+        public virtual void SetupGeohashGrid(int maxLevels)
+        {
+            this.ctx = SpatialContext.GEO;
+            //A fairly shallow grid, and default 2.5% distErrPct
+            if (maxLevels == -1)
+                maxLevels = randomIntBetween(1, 3);//max 16k cells (32^3)
+            this.grid = new GeohashPrefixTree(ctx, maxLevels);
+            this.strategy = new RecursivePrefixTreeStrategy(grid, GetType().Name);
+        }
+
+        [Test, Repeat(ITERATIONS)]
+        public virtual void TestIntersects()
+        {
+            SetupGrid(-1);
+            doTest(SpatialOperation.Intersects);
+        }
+
+        [Test, Repeat(ITERATIONS)]
+        public virtual void TestWithin()
+        {
+            SetupGrid(-1);
+            doTest(SpatialOperation.IsWithin);
+        }
+
+        [Test, Repeat(ITERATIONS)]
+        public virtual void TestContains()
+        {
+            SetupGrid(-1);
+            doTest(SpatialOperation.Contains);
+        }
+
+        [Test, Repeat(ITERATIONS)]
+        public virtual void TestDisjoint()
+        {
+            SetupGrid(-1);
+            doTest(SpatialOperation.IsDisjointTo);
+        }
+
+        /** See LUCENE-5062, {@link ContainsPrefixTreeFilter#multiOverlappingIndexedShapes}. */
+        [Test, Repeat(ITERATIONS)]
+        public virtual void TestContainsPairOverlap()
+        {
+            SetupQuadGrid(3);
+            adoc("0", new ShapePair(ctx.MakeRectangle(0, 33, -128, 128), ctx.MakeRectangle(33, 128, -128, 128), true, ctx));
+            Commit();
+            Query query = strategy.MakeQuery(new SpatialArgs(SpatialOperation.Contains,
+                ctx.MakeRectangle(0, 128, -16, 128)));
+            SearchResults searchResults = executeQuery(query, 1);
+            assertEquals(1, searchResults.numFound);
+        }
+
+        [Test]
+        public virtual void TestWithinDisjointParts()
+        {
+            SetupQuadGrid(7);
+            //one shape comprised of two parts, quite separated apart
+            adoc("0", new ShapePair(ctx.MakeRectangle(0, 10, -120, -100), ctx.MakeRectangle(220, 240, 110, 125), false, ctx));
+            Commit();
+            //query surrounds only the second part of the indexed shape
+            Query query = strategy.MakeQuery(new SpatialArgs(SpatialOperation.IsWithin,
+                ctx.MakeRectangle(210, 245, 105, 128)));
+            SearchResults searchResults = executeQuery(query, 1);
+            //we shouldn't find it because it's not completely within
+            assertTrue(searchResults.numFound == 0);
+        }
+
+        [Test] /** LUCENE-4916 */
+        public virtual void TestWithinLeafApproxRule()
+        {
+            SetupQuadGrid(2);//4x4 grid
+                             //indexed shape will simplify to entire right half (2 top cells)
+            adoc("0", ctx.MakeRectangle(192, 204, -128, 128));
+            Commit();
+
+            ((RecursivePrefixTreeStrategy)strategy).PrefixGridScanLevel = (Random().nextInt(2 + 1));
+
+            //query does NOT contain it; both indexed cells are leaves to the query, and
+            // when expanded to the full grid cells, the top one's top row is disjoint
+            // from the query and thus not a match.
+            assertTrue(executeQuery(strategy.MakeQuery(
+                new SpatialArgs(SpatialOperation.IsWithin, ctx.MakeRectangle(38, 192, -72, 56))
+            ), 1).numFound == 0);//no-match
+
+            //this time the rect is a little bigger and is considered a match. It's a
+            // an acceptable false-positive because of the grid approximation.
+            assertTrue(executeQuery(strategy.MakeQuery(
+                new SpatialArgs(SpatialOperation.IsWithin, ctx.MakeRectangle(38, 192, -72, 80))
+            ), 1).numFound == 1);//match
+        }
+
+        //Override so we can index parts of a pair separately, resulting in the detailLevel
+        // being independent for each shape vs the whole thing
+        protected override Document newDoc(String id, IShape shape)
+        {
+            Document doc = new Document();
+            doc.Add(new StringField("id", id, Field.Store.YES));
+            if (shape != null)
+            {
+                IList<IShape> shapes;
+                if (shape is ShapePair)
+                {
+                    shapes = new List<IShape>(2);
+                    shapes.Add(((ShapePair)shape).shape1);
+                    shapes.Add(((ShapePair)shape).shape2);
+                }
+                else
+                {
+                    shapes = new List<IShape>(new IShape[] { shape });//Collections.Singleton(shape);
+                }
+                foreach (IShape shapei in shapes)
+                {
+                    foreach (Field f in strategy.CreateIndexableFields(shapei))
+                    {
+                        doc.Add(f);
+                    }
+                }
+                if (storeShape)//just for diagnostics
+                    doc.Add(new StoredField(strategy.FieldName, shape.toString()));
+            }
+            return doc;
+        }
+
+        private void doTest(SpatialOperation operation)
+        {
+            //first show that when there's no data, a query will result in no results
+            {
+                Query query = strategy.MakeQuery(new SpatialArgs(operation, randomRectangle()));
+                SearchResults searchResults = executeQuery(query, 1);
+                assertEquals(0, searchResults.numFound);
+            }
+
+            bool biasContains = (operation == SpatialOperation.Contains);
+
+            //Main index loop:
+            IDictionary<String, IShape> indexedShapes = new LinkedHashMap<String, IShape>();
+            IDictionary<String, IShape> indexedShapesGS = new LinkedHashMap<String, IShape>();//grid snapped
+            int numIndexedShapes = randomIntBetween(1, 6);
+            bool indexedAtLeastOneShapePair = false;
+            for (int i = 0; i < numIndexedShapes; i++)
+            {
+                String id = "" + i;
+                IShape indexedShape;
+                int R = Random().nextInt(12);
+                if (R == 0)
+                {//1 in 12
+                    indexedShape = null;
+                }
+                else if (R == 1)
+                {//1 in 12
+                    indexedShape = randomPoint();//just one point
+                }
+                else if (R <= 4)
+                {//3 in 12
+                 //comprised of more than one shape
+                    indexedShape = randomShapePairRect(biasContains);
+                    indexedAtLeastOneShapePair = true;
+                }
+                else
+                {
+                    indexedShape = randomRectangle();//just one rect
+                }
+
+                indexedShapes.Put(id, indexedShape);
+                indexedShapesGS.Put(id, gridSnap(indexedShape));
+
+                adoc(id, indexedShape);
+
+                if (Random().nextInt(10) == 0)
+                    Commit();//intermediate commit, produces extra segments
+
+            }
+            //delete some documents randomly
+            IEnumerator<String> idIter = indexedShapes.Keys.ToList().GetEnumerator();
+            while (idIter.MoveNext())
+            {
+                String id = idIter.Current;
+                if (Random().nextInt(10) == 0)
+                {
+                    DeleteDoc(id);
+                    //idIter.Remove();
+                    indexedShapes.Remove(id); // LUCENENET TODO: Verify this works.
+                    indexedShapesGS.Remove(id);
+                }
+            }
+
+            Commit();
+
+            //Main query loop:
+            int numQueryShapes = AtLeast(20);
+            for (int i = 0; i < numQueryShapes; i++)
+            {
+                int scanLevel = randomInt(grid.MaxLevels);
+                ((RecursivePrefixTreeStrategy)strategy).PrefixGridScanLevel = (scanLevel);
+
+                IShape queryShape;
+                switch (randomInt(10))
+                {
+                    case 0: queryShape = randomPoint(); break;
+                    // LUCENE-5549
+                    //TODO debug: -Dtests.method=testWithin -Dtests.multiplier=3 -Dtests.seed=5F5294CE2E075A3E:AAD2F0F79288CA64
+                    //        case 1:case 2:case 3:
+                    //          if (!indexedAtLeastOneShapePair) { // avoids ShapePair.relate(ShapePair), which isn't reliable
+                    //            queryShape = randomShapePairRect(!biasContains);//invert biasContains for query side
+                    //            break;
+                    //          }
+                    default: queryShape = randomRectangle(); break;
+                }
+                IShape queryShapeGS = gridSnap(queryShape);
+
+                bool opIsDisjoint = operation == SpatialOperation.IsDisjointTo;
+
+                //Generate truth via brute force:
+                // We ensure true-positive matches (if the predicate on the raw shapes match
+                //  then the search should find those same matches).
+                // approximations, false-positive matches
+                ISet<String> expectedIds = new /* LinkedHashSet<string>*/ HashSet<string>();//true-positives
+                ISet<String> secondaryIds = new /* LinkedHashSet<string>*/ HashSet<string>();//false-positives (unless disjoint)
+                foreach (var entry in indexedShapes)
+                {
+                    String id = entry.Key;
+                    IShape indexedShapeCompare = entry.Value;
+                    if (indexedShapeCompare == null)
+                        continue;
+                    IShape queryShapeCompare = queryShape;
+
+                    if (operation.Evaluate(indexedShapeCompare, queryShapeCompare))
+                    {
+                        expectedIds.add(id);
+                        if (opIsDisjoint)
+                        {
+                            //if no longer intersect after buffering them, for disjoint, remember this
+                            indexedShapeCompare = indexedShapesGS[id];
+                            queryShapeCompare = queryShapeGS;
+                            if (!operation.Evaluate(indexedShapeCompare, queryShapeCompare))
+                                secondaryIds.add(id);
+                        }
+                    }
+                    else if (!opIsDisjoint)
+                    {
+                        //buffer either the indexed or query shape (via gridSnap) and try again
+                        if (operation == SpatialOperation.Intersects)
+                        {
+                            indexedShapeCompare = indexedShapesGS[id];
+                            queryShapeCompare = queryShapeGS;
+                            //TODO Unfortunately, grid-snapping both can result in intersections that otherwise
+                            // wouldn't happen when the grids are adjacent. Not a big deal but our test is just a
+                            // bit more lenient.
+                        }
+                        else if (operation == SpatialOperation.Contains)
+                        {
+                            indexedShapeCompare = indexedShapesGS[id];
+                        }
+                        else if (operation == SpatialOperation.IsWithin)
+                        {
+                            queryShapeCompare = queryShapeGS;
+                        }
+                        if (operation.Evaluate(indexedShapeCompare, queryShapeCompare))
+                            secondaryIds.add(id);
+                    }
+                }
+
+                //Search and verify results
+                SpatialArgs args = new SpatialArgs(operation, queryShape);
+                if (queryShape is ShapePair)
+                    args.DistErrPct = (0.0);//a hack; we want to be more detailed than gridSnap(queryShape)
+                Query query = strategy.MakeQuery(args);
+                SearchResults got = executeQuery(query, 100);
+                ISet<String> remainingExpectedIds = new /* LinkedHashSet<string>*/ HashSet<string>(expectedIds);
+                foreach (SearchResult result in got.results)
+                {
+                    String id = result.GetId();
+                    bool removed = remainingExpectedIds.remove(id);
+                    if (!removed && (!opIsDisjoint && !secondaryIds.contains(id)))
+                    {
+                        fail("Shouldn't match", id, indexedShapes, indexedShapesGS, queryShape);
+                    }
+                }
+                if (opIsDisjoint)
+                    remainingExpectedIds.removeAll(secondaryIds);
+                if (remainingExpectedIds.Any())
+                {
+                    var iter = remainingExpectedIds.GetEnumerator();
+                    iter.MoveNext();
+                    String id = iter.Current;
+                    fail("Should have matched", id, indexedShapes, indexedShapesGS, queryShape);
+                }
+            }
+        }
+
+        private IShape randomShapePairRect(bool biasContains)
+        {
+            IRectangle shape1 = randomRectangle();
+            IRectangle shape2 = randomRectangle();
+            return new ShapePair(shape1, shape2, biasContains, ctx);
+        }
+
+        private void fail(String label, String id, IDictionary<String, IShape> indexedShapes, IDictionary<String, IShape> indexedShapesGS, IShape queryShape)
+        {
+            Console.WriteLine("Ig:" + indexedShapesGS[id] + " Qg:" + gridSnap(queryShape));
+            fail(label + " I#" + id + ":" + indexedShapes[id] + " Q:" + queryShape);
+        }
+
+        //  private Rectangle inset(Rectangle r) {
+        //    //typically inset by 1 (whole numbers are easy to read)
+        //    double d = Math.min(1.0, grid.getDistanceForLevel(grid.getMaxLevels()) / 4);
+        //    return ctx.makeRectangle(r.getMinX() + d, r.getMaxX() - d, r.getMinY() + d, r.getMaxY() - d);
+        //  }
+
+        protected IShape gridSnap(IShape snapMe)
+        {
+            if (snapMe == null)
+                return null;
+            if (snapMe is ShapePair)
+            {
+                ShapePair me = (ShapePair)snapMe;
+                return new ShapePair(gridSnap(me.shape1), gridSnap(me.shape2), me.biasContainsThenWithin, ctx);
+            }
+            if (snapMe is IPoint)
+            {
+                snapMe = snapMe.BoundingBox;
+            }
+            //The next 4 lines mimic PrefixTreeStrategy.createIndexableFields()
+            double distErrPct = ((PrefixTreeStrategy)strategy).DistErrPct;
+            double distErr = SpatialArgs.CalcDistanceFromErrPct(snapMe, distErrPct, ctx);
+            int detailLevel = grid.GetLevelForDistance(distErr);
+            IList<Cell> cells = grid.GetCells(snapMe, detailLevel, false, true);
+
+            //calc bounding box of cells.
+            List<IShape> cellShapes = new List<IShape>(cells.size());
+            foreach (Cell cell in cells)
+            {
+                cellShapes.Add(cell.GetShape());
+            }
+            return new ShapeCollection(cellShapes, ctx).BoundingBox;
+        }
+
+        /**
+         * An aggregate of 2 shapes. Unfortunately we can't simply use a ShapeCollection because:
+         * (a) ambiguity between CONTAINS & WITHIN for equal shapes, and
+         * (b) adjacent pairs could as a whole contain the input shape.
+         * The tests here are sensitive to these matters, although in practice ShapeCollection
+         * is fine.
+         */
+        private class ShapePair : ShapeCollection /*<Shape>*/
+        {
+
+            private readonly SpatialContext ctx;
+            internal IShape shape1, shape2;
+            internal bool biasContainsThenWithin;//a hack
+
+            public ShapePair(IShape shape1, IShape shape2, bool containsThenWithin, SpatialContext ctx)
+                        : base(Arrays.AsList(shape1, shape2), ctx)
+            {
+
+                this.shape1 = shape1;
+                this.shape2 = shape2;
+                biasContainsThenWithin = containsThenWithin;
+            }
+
+            public override SpatialRelation Relate(IShape other)
+            {
+                SpatialRelation r = relateApprox(other);
+                if (r == SpatialRelation.CONTAINS)
+                    return r;
+                if (r == SpatialRelation.DISJOINT)
+                    return r;
+                if (r == SpatialRelation.WITHIN && !biasContainsThenWithin)
+                    return r;
+
+                //See if the correct answer is actually Contains, when the indexed shapes are adjacent,
+                // creating a larger shape that contains the input shape.
+                bool pairTouches = shape1.Relate(shape2).Intersects();
+                if (!pairTouches)
+                    return r;
+                //test all 4 corners
+                IRectangle oRect = (IRectangle)other;
+                if (Relate(ctx.MakePoint(oRect.MinX, oRect.MinY)) == SpatialRelation.CONTAINS
+                    && Relate(ctx.MakePoint(oRect.MinX, oRect.MaxY)) == SpatialRelation.CONTAINS
+                    && Relate(ctx.MakePoint(oRect.MaxX, oRect.MinY)) == SpatialRelation.CONTAINS
+                    && Relate(ctx.MakePoint(oRect.MaxX, oRect.MaxY)) == SpatialRelation.CONTAINS)
+                    return SpatialRelation.CONTAINS;
+                return r;
+            }
+
+            private SpatialRelation relateApprox(IShape other)
+            {
+                if (biasContainsThenWithin)
+                {
+                    if (shape1.Relate(other) == SpatialRelation.CONTAINS || shape1.equals(other)
+                        || shape2.Relate(other) == SpatialRelation.CONTAINS || shape2.equals(other)) return SpatialRelation.CONTAINS;
+
+                    if (shape1.Relate(other) == SpatialRelation.WITHIN && shape2.Relate(other) == SpatialRelation.WITHIN) return SpatialRelation.WITHIN;
+
+                }
+                else
+                {
+                    if ((shape1.Relate(other) == SpatialRelation.WITHIN || shape1.equals(other))
+                        && (shape2.Relate(other) == SpatialRelation.WITHIN || shape2.equals(other))) return SpatialRelation.WITHIN;
+
+                    if (shape1.Relate(other) == SpatialRelation.CONTAINS || shape2.Relate(other) == SpatialRelation.CONTAINS) return SpatialRelation.CONTAINS;
+                }
+
+                if (shape1.Relate(other).Intersects() || shape2.Relate(other).Intersects())
+                    return SpatialRelation.INTERSECTS;//might actually be 'CONTAINS' if the pair are adjacent but we handle that later
+                return SpatialRelation.DISJOINT;
+            }
+
+            public override String ToString()
+            {
+                return "ShapePair(" + shape1 + " , " + shape2 + ")";
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/d8c73530/src/Lucene.Net.Tests.Spatial/Prefix/TestRecursivePrefixTreeStrategy.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Tests.Spatial/Prefix/TestRecursivePrefixTreeStrategy.cs b/src/Lucene.Net.Tests.Spatial/Prefix/TestRecursivePrefixTreeStrategy.cs
new file mode 100644
index 0000000..f55b189
--- /dev/null
+++ b/src/Lucene.Net.Tests.Spatial/Prefix/TestRecursivePrefixTreeStrategy.cs
@@ -0,0 +1,127 @@
+\ufeffusing Lucene.Net.Spatial.Prefix.Tree;
+using Lucene.Net.Spatial.Queries;
+using NUnit.Framework;
+using Spatial4n.Core.Context;
+using Spatial4n.Core.Distance;
+using Spatial4n.Core.Shapes;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace Lucene.Net.Spatial.Prefix
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF 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.
+     */
+
+    public class TestRecursivePrefixTreeStrategy : StrategyTestCase
+    {
+        private int maxLength;
+
+        //Tests should call this first.
+        private void init(int maxLength)
+        {
+            this.maxLength = maxLength;
+            this.ctx = SpatialContext.GEO;
+            GeohashPrefixTree grid = new GeohashPrefixTree(ctx, maxLength);
+            this.strategy = new RecursivePrefixTreeStrategy(grid, GetType().Name);
+        }
+
+        [Test]
+        public virtual void TestFilterWithVariableScanLevel()
+        {
+            init(GeohashPrefixTree.MaxLevelsPossible);
+            getAddAndVerifyIndexedDocuments(DATA_WORLD_CITIES_POINTS);
+
+            //execute queries for each prefix grid scan level
+            for (int i = 0; i <= maxLength; i++)
+            {
+                ((RecursivePrefixTreeStrategy)strategy).PrefixGridScanLevel = (i);
+                executeQueries(SpatialMatchConcern.FILTER, QTEST_Cities_Intersects_BBox);
+            }
+        }
+
+        [Test]
+        public virtual void TestOneMeterPrecision()
+        {
+            init(GeohashPrefixTree.MaxLevelsPossible);
+            GeohashPrefixTree grid = (GeohashPrefixTree)((RecursivePrefixTreeStrategy)strategy).Grid;
+            //DWS: I know this to be true.  11 is needed for one meter
+            double degrees = DistanceUtils.Dist2Degrees(0.001, DistanceUtils.EARTH_MEAN_RADIUS_KM);
+            assertEquals(11, grid.GetLevelForDistance(degrees));
+        }
+
+        [Test]
+        public virtual void TestPrecision()
+        {
+            init(GeohashPrefixTree.MaxLevelsPossible);
+
+            Spatial4n.Core.Shapes.IPoint iPt = ctx.MakePoint(2.8028712999999925, 48.3708044);//lon, lat
+            AddDocument(newDoc("iPt", iPt));
+            Commit();
+
+            Spatial4n.Core.Shapes.IPoint qPt = ctx.MakePoint(2.4632387000000335, 48.6003516);
+
+            double KM2DEG = DistanceUtils.Dist2Degrees(1, DistanceUtils.EARTH_MEAN_RADIUS_KM);
+            double DEG2KM = 1 / KM2DEG;
+
+            double DIST = 35.75;//35.7499...
+            assertEquals(DIST, ctx.DistCalc.Distance(iPt, qPt) * DEG2KM, 0.001);
+
+            //distErrPct will affect the query shape precision. The indexed precision
+            // was set to nearly zilch via init(GeohashPrefixTree.getMaxLevelsPossible());
+            double distErrPct = 0.025; //the suggested default, by the way
+            double distMult = 1 + distErrPct;
+
+            assertTrue(35.74 * distMult >= DIST);
+            checkHits(q(qPt, 35.74 * KM2DEG, distErrPct), 1, null);
+
+            assertTrue(30 * distMult < DIST);
+            checkHits(q(qPt, 30 * KM2DEG, distErrPct), 0, null);
+
+            assertTrue(33 * distMult < DIST);
+            checkHits(q(qPt, 33 * KM2DEG, distErrPct), 0, null);
+
+            assertTrue(34 * distMult < DIST);
+            checkHits(q(qPt, 34 * KM2DEG, distErrPct), 0, null);
+        }
+
+        private SpatialArgs q(Spatial4n.Core.Shapes.IPoint pt, double distDEG, double distErrPct)
+        {
+            IShape shape = ctx.MakeCircle(pt, distDEG);
+            SpatialArgs args = new SpatialArgs(SpatialOperation.Intersects, shape);
+            args.DistErrPct = (distErrPct);
+            return args;
+        }
+
+        private void checkHits(SpatialArgs args, int assertNumFound, int[] assertIds)
+        {
+            SearchResults got = executeQuery(strategy.MakeQuery(args), 100);
+            assertEquals("" + args, assertNumFound, got.numFound);
+            if (assertIds != null)
+            {
+                ISet<int?> gotIds = new HashSet<int?>();
+                foreach (SearchResult result in got.results)
+                {
+                    gotIds.add(int.Parse(result.document.Get("id"), CultureInfo.InvariantCulture));
+                }
+                foreach (int assertId in assertIds)
+                {
+                    assertTrue("has " + assertId, gotIds.contains(assertId));
+                }
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/d8c73530/src/Lucene.Net.Tests.Spatial/Prefix/TestTermQueryPrefixGridStrategy.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Tests.Spatial/Prefix/TestTermQueryPrefixGridStrategy.cs b/src/Lucene.Net.Tests.Spatial/Prefix/TestTermQueryPrefixGridStrategy.cs
new file mode 100644
index 0000000..02afd84
--- /dev/null
+++ b/src/Lucene.Net.Tests.Spatial/Prefix/TestTermQueryPrefixGridStrategy.cs
@@ -0,0 +1,60 @@
+\ufeffusing Lucene.Net.Documents;
+using Lucene.Net.Index;
+using Lucene.Net.Spatial.Prefix.Tree;
+using Lucene.Net.Spatial.Queries;
+using Lucene.Net.Support;
+using NUnit.Framework;
+using Spatial4n.Core.Context;
+
+namespace Lucene.Net.Spatial.Prefix
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF 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.
+     */
+
+    public class TestTermQueryPrefixGridStrategy : SpatialTestCase
+    {
+        [Test]
+        public virtual void TestNGramPrefixGridLosAngeles()
+        {
+            SpatialContext ctx = SpatialContext.GEO;
+            TermQueryPrefixTreeStrategy prefixGridStrategy = new TermQueryPrefixTreeStrategy(new QuadPrefixTree(ctx), "geo");
+
+            Spatial4n.Core.Shapes.IShape point = ctx.MakePoint(-118.243680, 34.052230);
+
+            Document losAngeles = new Document();
+            losAngeles.Add(new StringField("name", "Los Angeles", Field.Store.YES));
+            foreach (IndexableField field in prefixGridStrategy.CreateIndexableFields(point))
+            {
+                losAngeles.Add(field);
+            }
+            losAngeles.Add(new StoredField(prefixGridStrategy.FieldName, point.toString()));//just for diagnostics
+
+            addDocumentsAndCommit(Arrays.AsList(losAngeles));
+
+            // This won't work with simple spatial context...
+            SpatialArgsParser spatialArgsParser = new SpatialArgsParser();
+            // TODO... use a non polygon query
+            //    SpatialArgs spatialArgs = spatialArgsParser.parse(
+            //        "Intersects(POLYGON((-127.00390625 39.8125,-112.765625 39.98828125,-111.53515625 31.375,-125.94921875 30.14453125,-127.00390625 39.8125)))",
+            //        new SimpleSpatialContext());
+
+            //    Query query = prefixGridStrategy.makeQuery(spatialArgs, fieldInfo);
+            //    SearchResults searchResults = executeQuery(query, 1);
+            //    assertEquals(1, searchResults.numFound);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/d8c73530/src/Lucene.Net.Tests.Spatial/Prefix/Tree/SpatialPrefixTreeTest.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Tests.Spatial/Prefix/Tree/SpatialPrefixTreeTest.cs b/src/Lucene.Net.Tests.Spatial/Prefix/Tree/SpatialPrefixTreeTest.cs
new file mode 100644
index 0000000..581b2f6
--- /dev/null
+++ b/src/Lucene.Net.Tests.Spatial/Prefix/Tree/SpatialPrefixTreeTest.cs
@@ -0,0 +1,105 @@
+\ufeffusing Lucene.Net.Documents;
+using Lucene.Net.Search;
+using Lucene.Net.Spatial.Queries;
+using NUnit.Framework;
+using Spatial4n.Core.Context;
+using Spatial4n.Core.Shapes;
+using System;
+
+namespace Lucene.Net.Spatial.Prefix.Tree
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF 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.
+     */
+
+    public class SpatialPrefixTreeTest : SpatialTestCase
+    {
+        //TODO plug in others and test them
+        new private SpatialContext ctx;
+        private SpatialPrefixTree trie;
+
+        public override void SetUp()
+        {
+            base.SetUp();
+            ctx = SpatialContext.GEO;
+        }
+
+        [Test]
+        public virtual void TestCellTraverse()
+        {
+            trie = new GeohashPrefixTree(ctx, 4);
+
+            Cell prevC = null;
+            Cell c = trie.WorldCell;
+            assertEquals(0, c.Level);
+            assertEquals(ctx.WorldBounds, c.GetShape());
+            while (c.Level < trie.MaxLevels)
+            {
+                prevC = c;
+
+                var iter = c.GetSubCells().GetEnumerator();
+                iter.MoveNext();
+                c = iter.Current;
+                //c = c.GetSubCells().GetEnumerator().next();//TODO random which one?
+
+                assertEquals(prevC.Level + 1, c.Level);
+                IRectangle prevNShape = (IRectangle)prevC.GetShape();
+                IShape s = c.GetShape();
+                IRectangle sbox = s.BoundingBox;
+                assertTrue(prevNShape.Width > sbox.Width);
+                assertTrue(prevNShape.Height > sbox.Height);
+            }
+        }
+        /**
+         * A PrefixTree pruning optimization gone bad.
+         * See <a href="https://issues.apache.org/jira/browse/LUCENE-4770>LUCENE-4770</a>.
+         */
+        [Test]
+        public virtual void TestBadPrefixTreePrune()
+        {
+
+            trie = new QuadPrefixTree(ctx, 12);
+            TermQueryPrefixTreeStrategy strategy = new TermQueryPrefixTreeStrategy(trie, "geo");
+            Document doc = new Document();
+            doc.Add(new TextField("id", "1", Field.Store.YES));
+
+            IShape area = ctx.MakeRectangle(-122.82, -122.78, 48.54, 48.56);
+
+            Field[] fields = strategy.CreateIndexableFields(area, 0.025);
+            foreach (Field field in fields)
+            {
+                doc.Add(field);
+            }
+            AddDocument(doc);
+
+            IPoint upperleft = ctx.MakePoint(-122.88, 48.54);
+            IPoint lowerright = ctx.MakePoint(-122.82, 48.62);
+
+            Query query = strategy.MakeQuery(new SpatialArgs(SpatialOperation.Intersects, ctx.MakeRectangle(upperleft, lowerright)));
+
+            Commit();
+
+            TopDocs search = indexSearcher.Search(query, 10);
+            ScoreDoc[] scoreDocs = search.ScoreDocs;
+            foreach (ScoreDoc scoreDoc in scoreDocs)
+            {
+                Console.WriteLine(indexSearcher.Doc(scoreDoc.Doc));
+            }
+
+            assertEquals(1, search.TotalHits);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/d8c73530/src/Lucene.Net.Tests.Spatial/Properties/AssemblyInfo.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Tests.Spatial/Properties/AssemblyInfo.cs b/src/Lucene.Net.Tests.Spatial/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..3e1f0a5
--- /dev/null
+++ b/src/Lucene.Net.Tests.Spatial/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+\ufeffusing System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Lucene.Net.Tests.Spatial")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Lucene.Net.Tests.Spatial")]
+[assembly: AssemblyCopyright("Copyright �  2016")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("31f52f5c-a08f-4363-8003-23d6f7d6eb3a")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/d8c73530/src/Lucene.Net.Tests.Spatial/Query/SpatialArgsParserTest.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Tests.Spatial/Query/SpatialArgsParserTest.cs b/src/Lucene.Net.Tests.Spatial/Query/SpatialArgsParserTest.cs
new file mode 100644
index 0000000..ee2f9a2
--- /dev/null
+++ b/src/Lucene.Net.Tests.Spatial/Query/SpatialArgsParserTest.cs
@@ -0,0 +1,69 @@
+\ufeffusing Lucene.Net.Util;
+using NUnit.Framework;
+using Spatial4n.Core.Context;
+using Spatial4n.Core.Shapes;
+using System;
+
+namespace Lucene.Net.Spatial.Queries
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF 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.
+     */
+
+    public class SpatialArgsParserTest : LuceneTestCase
+    {
+        private SpatialContext ctx = SpatialContext.GEO;
+
+        //The args parser is only dependent on the ctx for IO so I don't care to test
+        // with other implementations.
+
+        [Test]
+        public virtual void TestArgsParser()
+        {
+            SpatialArgsParser parser = new SpatialArgsParser();
+
+            String arg = SpatialOperation.IsWithin + "(Envelope(-10, 10, 20, -20))";
+            SpatialArgs @out = parser.Parse(arg, ctx);
+            assertEquals(SpatialOperation.IsWithin, @out.Operation);
+            IRectangle bounds = (IRectangle)@out.Shape;
+            assertEquals(-10.0, bounds.MinX, 0D);
+            assertEquals(10.0, bounds.MaxX, 0D);
+
+            // Disjoint should not be scored
+            arg = SpatialOperation.IsDisjointTo + " (Envelope(-10,-20,20,10))";
+            @out = parser.Parse(arg, ctx);
+            assertEquals(SpatialOperation.IsDisjointTo, @out.Operation);
+
+            try
+            {
+                parser.Parse(SpatialOperation.IsDisjointTo + "[ ]", ctx);
+                fail("spatial operations need args");
+            }
+            catch (Exception ex)
+            {//expected
+            }
+
+            try
+            {
+                parser.Parse("XXXX(Envelope(-10, 10, 20, -20))", ctx);
+                fail("unknown operation!");
+            }
+            catch (Exception ex)
+            {//expected
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/d8c73530/src/Lucene.Net.Tests.Spatial/QueryEqualsHashCodeTest.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Tests.Spatial/QueryEqualsHashCodeTest.cs b/src/Lucene.Net.Tests.Spatial/QueryEqualsHashCodeTest.cs
new file mode 100644
index 0000000..25db926
--- /dev/null
+++ b/src/Lucene.Net.Tests.Spatial/QueryEqualsHashCodeTest.cs
@@ -0,0 +1,169 @@
+\ufeffusing Lucene.Net.Spatial.Prefix;
+using Lucene.Net.Spatial.Prefix.Tree;
+using Lucene.Net.Spatial.Queries;
+using Lucene.Net.Spatial.Serialized;
+using Lucene.Net.Spatial.Vector;
+using Lucene.Net.Util;
+using NUnit.Framework;
+using Spatial4n.Core.Context;
+using Spatial4n.Core.Shapes;
+using System;
+using System.Collections.Generic;
+
+namespace Lucene.Net.Spatial
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF 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.
+     */
+
+    public class QueryEqualsHashCodeTest : LuceneTestCase
+    {
+        private readonly SpatialContext ctx = SpatialContext.GEO;
+
+        [Test]
+        public virtual void TestEqualsHashCode()
+        {
+
+            SpatialPrefixTree gridQuad = new QuadPrefixTree(ctx, 10);
+            SpatialPrefixTree gridGeohash = new GeohashPrefixTree(ctx, 10);
+
+            List<SpatialStrategy> strategies = new List<SpatialStrategy>();
+            strategies.Add(new RecursivePrefixTreeStrategy(gridGeohash, "recursive_geohash"));
+            strategies.Add(new TermQueryPrefixTreeStrategy(gridQuad, "termquery_quad"));
+            strategies.Add(new PointVectorStrategy(ctx, "pointvector"));
+            //strategies.Add(new BBoxStrategy(ctx, "bbox"));
+            strategies.Add(new SerializedDVStrategy(ctx, "serialized"));
+            foreach (SpatialStrategy strategy in strategies)
+            {
+                TestEqualsHashcode(strategy);
+            }
+        }
+
+        private class ObjGeneratorQueryAnonymousHelper : ObjGenerator
+        {
+            private readonly SpatialStrategy strategy;
+
+            public ObjGeneratorQueryAnonymousHelper(SpatialStrategy strategy)
+            {
+                this.strategy = strategy;
+            }
+
+            public object gen(SpatialArgs args)
+            {
+                return strategy.MakeQuery(args);
+            }
+        }
+
+        private class ObjGeneratorFilterAnonymousHelper : ObjGenerator
+        {
+            private readonly SpatialStrategy strategy;
+
+            public ObjGeneratorFilterAnonymousHelper(SpatialStrategy strategy)
+            {
+                this.strategy = strategy;
+            }
+
+            public object gen(SpatialArgs args)
+            {
+                return strategy.MakeFilter(args);
+            }
+        }
+
+        private class ObjGeneratorDistanceValueSourceAnonymousHelper : ObjGenerator
+        {
+            private readonly SpatialStrategy strategy;
+
+            public ObjGeneratorDistanceValueSourceAnonymousHelper(SpatialStrategy strategy)
+            {
+                this.strategy = strategy;
+            }
+
+            public object gen(SpatialArgs args)
+            {
+                return strategy.MakeDistanceValueSource(args.Shape.Center);
+            }
+        }
+
+        private void TestEqualsHashcode(SpatialStrategy strategy)
+        {
+            SpatialArgs args1 = MakeArgs1();
+            SpatialArgs args2 = MakeArgs2();
+            TestEqualsHashcode(args1, args2, new ObjGeneratorQueryAnonymousHelper(strategy));
+            //        testEqualsHashcode(args1, args2, new ObjGenerator() {
+            //  @Override
+            //  public Object gen(SpatialArgs args)
+            //    {
+            //        return strategy.makeQuery(args);
+            //    }
+            //});
+            TestEqualsHashcode(args1, args2, new ObjGeneratorFilterAnonymousHelper(strategy));
+            //    testEqualsHashcode(args1, args2, new ObjGenerator()
+            //    {
+            //        @Override
+            //      public Object gen(SpatialArgs args)
+            //    {
+            //        return strategy.makeFilter(args);
+            //    }
+            //});
+            TestEqualsHashcode(args1, args2, new ObjGeneratorDistanceValueSourceAnonymousHelper(strategy));
+            //            testEqualsHashcode(args1, args2, new ObjGenerator()
+            //{
+            //    @Override
+            //      public Object gen(SpatialArgs args)
+            //{
+            //    return strategy.makeDistanceValueSource(args.getShape().getCenter());
+            //}
+            //    });
+        }
+
+        private void TestEqualsHashcode(SpatialArgs args1, SpatialArgs args2, ObjGenerator generator)
+        {
+            Object first;
+            try
+            {
+                first = generator.gen(args1);
+            }
+            catch (NotSupportedException e)
+            {
+                return;
+            }
+            if (first == null)
+                return;//unsupported op?
+            Object second = generator.gen(args1);//should be the same
+            assertEquals(first, second);
+            assertEquals(first.GetHashCode(), second.GetHashCode());
+            second = generator.gen(args2);//now should be different
+            assertNotSame(args1, args2);
+        }
+
+        private SpatialArgs MakeArgs1()
+        {
+            IShape shape1 = ctx.MakeRectangle(0, 0, 10, 10);
+            return new SpatialArgs(SpatialOperation.Intersects, shape1);
+        }
+
+        private SpatialArgs MakeArgs2()
+        {
+            IShape shape2 = ctx.MakeRectangle(0, 0, 20, 20);
+            return new SpatialArgs(SpatialOperation.Intersects, shape2);
+        }
+
+        interface ObjGenerator
+        {
+            Object gen(SpatialArgs args);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/d8c73530/src/Lucene.Net.Tests.Spatial/Serialized/SerializedStrategyTest.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Tests.Spatial/Serialized/SerializedStrategyTest.cs b/src/Lucene.Net.Tests.Spatial/Serialized/SerializedStrategyTest.cs
new file mode 100644
index 0000000..b4d00a6
--- /dev/null
+++ b/src/Lucene.Net.Tests.Spatial/Serialized/SerializedStrategyTest.cs
@@ -0,0 +1,73 @@
+\ufeffusing Lucene.Net.Search;
+using NUnit.Framework;
+using Spatial4n.Core.Context;
+
+namespace Lucene.Net.Spatial.Serialized
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF 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.
+     */
+
+    public class SerializedStrategyTest : StrategyTestCase
+    {
+        public override void SetUp()
+        {
+            base.SetUp();
+            this.ctx = SpatialContext.GEO;
+            this.strategy = new SerializedDVStrategy(ctx, "serialized");
+        }
+
+
+        protected override bool NeedsDocValues()
+        {
+            return true;
+        }
+
+        //called by StrategyTestCase; we can't let it call our makeQuery which will UOE ex.
+        protected override Query makeQuery(SpatialTestQuery q)
+        {
+            return new FilteredQuery(new MatchAllDocsQuery(), strategy.MakeFilter(q.args),
+                FilteredQuery.QUERY_FIRST_FILTER_STRATEGY);
+        }
+
+        [Test]
+        public virtual void TestBasicOperaions()
+        {
+            getAddAndVerifyIndexedDocuments(DATA_SIMPLE_BBOX);
+
+            executeQueries(SpatialMatchConcern.EXACT, QTEST_Simple_Queries_BBox);
+        }
+
+        [Test]
+        public virtual void TestStatesBBox()
+        {
+            getAddAndVerifyIndexedDocuments(DATA_STATES_BBOX);
+
+            executeQueries(SpatialMatchConcern.FILTER, QTEST_States_IsWithin_BBox);
+            executeQueries(SpatialMatchConcern.FILTER, QTEST_States_Intersects_BBox);
+        }
+
+        [Test]
+        public virtual void TestCitiesIntersectsBBox()
+        {
+            getAddAndVerifyIndexedDocuments(DATA_WORLD_CITIES_POINTS);
+
+            executeQueries(SpatialMatchConcern.FILTER, QTEST_Cities_Intersects_BBox);
+        }
+
+        //sorting is tested in DistanceStrategyTest
+    }
+}

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/d8c73530/src/Lucene.Net.Tests.Spatial/SpatialArgsTest.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Tests.Spatial/SpatialArgsTest.cs b/src/Lucene.Net.Tests.Spatial/SpatialArgsTest.cs
new file mode 100644
index 0000000..afac670
--- /dev/null
+++ b/src/Lucene.Net.Tests.Spatial/SpatialArgsTest.cs
@@ -0,0 +1,52 @@
+\ufeffusing Lucene.Net.Spatial.Queries;
+using Lucene.Net.Util;
+using NUnit.Framework;
+using Spatial4n.Core.Context;
+using Spatial4n.Core.Shapes;
+
+namespace Lucene.Net.Spatial
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF 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.
+     */
+
+    public class SpatialArgsTest : LuceneTestCase
+    {
+        [Test]
+        public void CalcDistanceFromErrPct()
+        {
+            SpatialContext ctx = SpatialContext.GEO;
+            double DEP = 0.5;//distErrPct
+
+            //the result is the diagonal distance from the center to the closest corner,
+            // times distErrPct
+
+            IShape superwide = ctx.MakeRectangle(-180, 180, 0, 0);
+            //0 distErrPct means 0 distance always
+            assertEquals(0, SpatialArgs.CalcDistanceFromErrPct(superwide, 0, ctx), 0);
+            assertEquals(180 * DEP, SpatialArgs.CalcDistanceFromErrPct(superwide, DEP, ctx), 0);
+
+            IShape supertall = ctx.MakeRectangle(0, 0, -90, 90);
+            assertEquals(90 * DEP, SpatialArgs.CalcDistanceFromErrPct(supertall, DEP, ctx), 0);
+
+            IShape upperhalf = ctx.MakeRectangle(-180, 180, 0, 90);
+            assertEquals(45 * DEP, SpatialArgs.CalcDistanceFromErrPct(upperhalf, DEP, ctx), 0.0001);
+
+            IShape midCircle = ctx.MakeCircle(0, 0, 45);
+            assertEquals(60 * DEP, SpatialArgs.CalcDistanceFromErrPct(midCircle, DEP, ctx), 0.0001);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/d8c73530/src/Lucene.Net.Tests.Spatial/SpatialExample.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Tests.Spatial/SpatialExample.cs b/src/Lucene.Net.Tests.Spatial/SpatialExample.cs
new file mode 100644
index 0000000..7e0f7f7
--- /dev/null
+++ b/src/Lucene.Net.Tests.Spatial/SpatialExample.cs
@@ -0,0 +1,196 @@
+\ufeffusing Lucene.Net.Documents;
+using Lucene.Net.Index;
+using Lucene.Net.Queries.Function;
+using Lucene.Net.Search;
+using Lucene.Net.Spatial.Prefix;
+using Lucene.Net.Spatial.Prefix.Tree;
+using Lucene.Net.Spatial.Queries;
+using Lucene.Net.Store;
+using Lucene.Net.Util;
+using NUnit.Framework;
+using Spatial4n.Core.Context;
+using Spatial4n.Core.Distance;
+using Spatial4n.Core.Shapes;
+using System;
+using System.Globalization;
+
+namespace Lucene.Net.Spatial
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF 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.
+     */
+
+    /// <summary>
+    /// This class serves as example code to show how to use the Lucene spatial
+    /// module.
+    /// </summary>
+    public class SpatialExample : LuceneTestCase
+    {
+        //Note: Test invoked via TestTestFramework.spatialExample()
+        public static void Main(String[] args)
+        {
+            new SpatialExample().Test();
+        }
+
+        [Test]
+        public virtual void Test()
+        {
+            Init();
+            IndexPoints();
+            Search();
+        }
+
+        /**
+         * The Spatial4j {@link SpatialContext} is a sort of global-ish singleton
+         * needed by Lucene spatial.  It's a facade to the rest of Spatial4j, acting
+         * as a factory for {@link Shape}s and provides access to reading and writing
+         * them from Strings.
+         */
+        private SpatialContext ctx;//"ctx" is the conventional variable name
+
+        /**
+         * The Lucene spatial {@link SpatialStrategy} encapsulates an approach to
+         * indexing and searching shapes, and providing distance values for them.
+         * It's a simple API to unify different approaches. You might use more than
+         * one strategy for a shape as each strategy has its strengths and weaknesses.
+         * <p />
+         * Note that these are initialized with a field name.
+         */
+        private SpatialStrategy strategy;
+
+        private Directory directory;
+
+        protected void Init()
+        {
+            //Typical geospatial context
+            //  These can also be constructed from SpatialContextFactory
+            this.ctx = SpatialContext.GEO;
+
+            int maxLevels = 11;//results in sub-meter precision for geohash
+                               //TODO demo lookup by detail distance
+                               //  This can also be constructed from SpatialPrefixTreeFactory
+            SpatialPrefixTree grid = new GeohashPrefixTree(ctx, maxLevels);
+
+            this.strategy = new RecursivePrefixTreeStrategy(grid, "myGeoField");
+
+            this.directory = new RAMDirectory();
+        }
+
+        private void IndexPoints()
+        {
+            IndexWriterConfig iwConfig = new IndexWriterConfig(TEST_VERSION_CURRENT, null);
+            IndexWriter indexWriter = new IndexWriter(directory, iwConfig);
+
+            //Spatial4j is x-y order for arguments
+            indexWriter.AddDocument(NewSampleDocument(
+                2, ctx.MakePoint(-80.93, 33.77)));
+
+            //Spatial4j has a WKT parser which is also "x y" order
+            indexWriter.AddDocument(NewSampleDocument(
+                4, ctx.ReadShapeFromWkt("POINT(60.9289094 -50.7693246)")));
+
+            indexWriter.AddDocument(NewSampleDocument(
+                20, ctx.MakePoint(0.1, 0.1), ctx.MakePoint(0, 0)));
+
+            indexWriter.Dispose();
+        }
+
+        private Document NewSampleDocument(int id, params IShape[] shapes)
+        {
+            Document doc = new Document();
+            doc.Add(new IntField("id", id, Field.Store.YES));
+            //Potentially more than one shape in this field is supported by some
+            // strategies; see the javadocs of the SpatialStrategy impl to see.
+            foreach (IShape shape in shapes)
+            {
+                foreach (IndexableField f in strategy.CreateIndexableFields(shape))
+                {
+                    doc.Add(f);
+                }
+                //store it too; the format is up to you
+                //  (assume point in this example)
+                IPoint pt = (IPoint)shape;
+                doc.Add(new StoredField(strategy.FieldName, pt.X + " " + pt.Y));
+            }
+
+            return doc;
+        }
+
+        private void Search()
+        {
+            IndexReader indexReader = DirectoryReader.Open(directory);
+            IndexSearcher indexSearcher = new IndexSearcher(indexReader);
+            Sort idSort = new Sort(new SortField("id", SortField.Type_e.INT));
+
+            //--Filter by circle (<= distance from a point)
+            {
+                //Search with circle
+                //note: SpatialArgs can be parsed from a string
+                SpatialArgs args = new SpatialArgs(SpatialOperation.Intersects,
+                    ctx.MakeCircle(-80.0, 33.0, DistanceUtils.Dist2Degrees(200, DistanceUtils.EARTH_MEAN_RADIUS_KM)));
+                Filter filter = strategy.MakeFilter(args);
+                TopDocs docs = indexSearcher.Search(new MatchAllDocsQuery(), filter, 10, idSort);
+                AssertDocMatchedIds(indexSearcher, docs, 2);
+                //Now, lets get the distance for the 1st doc via computing from stored point value:
+                // (this computation is usually not redundant)
+                Document doc1 = indexSearcher.Doc(docs.ScoreDocs[0].Doc);
+                String doc1Str = doc1.GetField(strategy.FieldName).StringValue;
+                //assume doc1Str is "x y" as written in newSampleDocument()
+                int spaceIdx = doc1Str.IndexOf(' ');
+                double x = double.Parse(doc1Str.Substring(0, spaceIdx - 0), CultureInfo.InvariantCulture);
+                double y = double.Parse(doc1Str.Substring(spaceIdx + 1), CultureInfo.InvariantCulture);
+                double doc1DistDEG = ctx.CalcDistance(args.Shape.Center, x, y);
+                assertEquals(121.6d, DistanceUtils.Degrees2Dist(doc1DistDEG, DistanceUtils.EARTH_MEAN_RADIUS_KM), 0.1);
+                //or more simply:
+                assertEquals(121.6d, doc1DistDEG * DistanceUtils.DEG_TO_KM, 0.1);
+            }
+            //--Match all, order by distance ascending
+            {
+                IPoint pt = ctx.MakePoint(60, -50);
+                ValueSource valueSource = strategy.MakeDistanceValueSource(pt, DistanceUtils.DEG_TO_KM);//the distance (in km)
+                Sort distSort = new Sort(valueSource.GetSortField(false)).Rewrite(indexSearcher);//false=asc dist
+                TopDocs docs = indexSearcher.Search(new MatchAllDocsQuery(), 10, distSort);
+                AssertDocMatchedIds(indexSearcher, docs, 4, 20, 2);
+                //To get the distance, we could compute from stored values like earlier.
+                // However in this example we sorted on it, and the distance will get
+                // computed redundantly.  If the distance is only needed for the top-X
+                // search results then that's not a big deal. Alternatively, try wrapping
+                // the ValueSource with CachingDoubleValueSource then retrieve the value
+                // from the ValueSource now. See LUCENE-4541 for an example.
+            }
+            //demo arg parsing
+            {
+                SpatialArgs args = new SpatialArgs(SpatialOperation.Intersects,
+                    ctx.MakeCircle(-80.0, 33.0, 1));
+                SpatialArgs args2 = new SpatialArgsParser().Parse("Intersects(BUFFER(POINT(-80 33),1))", ctx);
+                assertEquals(args.toString(), args2.toString());
+            }
+
+            indexReader.Dispose();
+        }
+
+        private void AssertDocMatchedIds(IndexSearcher indexSearcher, TopDocs docs, params int[] ids)
+        {
+            int[]
+            gotIds = new int[docs.TotalHits];
+            for (int i = 0; i < gotIds.Length; i++)
+            {
+                gotIds[i] = Convert.ToInt32(indexSearcher.Doc(docs.ScoreDocs[i].Doc).GetField("id").NumericValue, CultureInfo.InvariantCulture);
+            }
+            assertArrayEquals(ids, gotIds);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/d8c73530/src/Lucene.Net.Tests.Spatial/SpatialMatchConcern.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Tests.Spatial/SpatialMatchConcern.cs b/src/Lucene.Net.Tests.Spatial/SpatialMatchConcern.cs
new file mode 100644
index 0000000..035f279
--- /dev/null
+++ b/src/Lucene.Net.Tests.Spatial/SpatialMatchConcern.cs
@@ -0,0 +1,35 @@
+\ufeffnamespace Lucene.Net.Spatial
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF 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.
+     */
+
+    public class SpatialMatchConcern
+    {
+        public readonly bool orderIsImportant;
+        public readonly bool resultsAreSuperset; // if the strategy can not give exact answers, but used to limit results
+
+        private SpatialMatchConcern(bool order, bool superset)
+        {
+            this.orderIsImportant = order;
+            this.resultsAreSuperset = superset;
+        }
+
+        public static readonly SpatialMatchConcern EXACT = new SpatialMatchConcern(true, false);
+        public static readonly SpatialMatchConcern FILTER = new SpatialMatchConcern(false, false);
+        public static readonly SpatialMatchConcern SUPERSET = new SpatialMatchConcern(false, true);
+    }
+}