diff --git a/hoot-core/src/main/cpp/hoot/core/conflate/poi-polygon/PoiPolygonSchema.cpp b/hoot-core/src/main/cpp/hoot/core/conflate/poi-polygon/PoiPolygonSchema.cpp
new file mode 100644
index 0000000..8bc90de
--- /dev/null
+++ b/hoot-core/src/main/cpp/hoot/core/conflate/poi-polygon/PoiPolygonSchema.cpp
@@ -0,0 +1,264 @@
+/*
+ * This file is part of Hootenanny.
+ *
+ * Hootenanny is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * --------------------------------------------------------------------
+ *
+ * The following copyright notices are generated automatically. If you
+ * have a new notice to add, please use the format:
+ * " * @copyright Copyright ..."
+ * This will properly maintain the copyright information. DigitalGlobe
+ * copyrights will be updated automatically.
+ *
+ * @copyright Copyright (C) 2020 DigitalGlobe (http://www.digitalglobe.com/)
+ */
+#include "PoiPolygonSchema.h"
+
+// hoot
+#include <hoot/core/criterion/BuildingCriterion.h>
+#include <hoot/core/schema/MetadataTags.h>
+#include <hoot/core/schema/OsmSchema.h>
+#include <hoot/core/util/ConfigOptions.h>
+#include <hoot/core/util/FileUtils.h>
+
+// Qt
+#include <QSet>
+#include <QStringBuilder>
+
+namespace hoot
+{
+
+QMultiHash<QString, QString> PoiPolygonSchema::_typeToNames;
+QSet<QString> PoiPolygonSchema::_allTypeKeys;
+
+PoiPolygonSchema::PoiPolygonSchema()
+{
+ _readTypeToNames();
+}
+
+void PoiPolygonSchema::_readTypeToNames()
+{
+ // see related note in ImplicitTagUtils::_modifyUndesirableTokens
+ if (_typeToNames.isEmpty())
+ {
+ const QStringList typeToNamesRaw =
+ FileUtils::readFileToList(ConfigOptions().getPoiPolygonTypeToNamesFile());
+ for (int i = 0; i < typeToNamesRaw.size(); i++)
+ {
+ const QString typeToNamesRawEntry = typeToNamesRaw.at(i);
+ const QStringList typeToNamesRawEntryParts = typeToNamesRawEntry.split(";");
+ if (typeToNamesRawEntryParts.size() != 2)
+ {
+ throw HootException("Invalid POI/Polygon type to names entry: " + typeToNamesRawEntry);
+ }
+ const QString kvp = typeToNamesRawEntryParts.at(0);
+ const QStringList names = typeToNamesRawEntryParts.at(1).split(",");
+ for (int j = 0; j < names.size(); j++)
+ {
+ _typeToNames.insert(kvp, names.at(j));
+ }
+ }
+ }
+}
+
+bool PoiPolygonSchema::_typeHasName(const QString& kvp, const QString& name)
+{
+ const QStringList typeNames =_typeToNames.values(kvp);
+ for (int i = 0; i < typeNames.size(); i++)
+ {
+ if (name.contains(typeNames.at(i)))
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+QString PoiPolygonSchema::_getMatchingTypeName(const QString& kvp, const QString& name)
+{
+ const QStringList typeNames =_typeToNames.values(kvp);
+ for (int i = 0; i < typeNames.size(); i++)
+ {
+ const QString typeName = typeNames.at(i);
+ if (name.contains(typeName))
+ {
+ return typeName;
+ }
+ }
+ return "";
+}
+
+bool PoiPolygonSchema::_haveMatchingTypeNames(const QString& kvp, const QString& name1,
+ const QString& name2)
+{
+ const QString typeName1 = _getMatchingTypeName(kvp, name1);
+ const QString typeName2 = _getMatchingTypeName(kvp, name2);
+ return typeName1 == typeName2 && !typeName1.isEmpty();
+}
+
+bool PoiPolygonSchema::isSchool(const ConstElementPtr& element)
+{
+ const QString amenityStr = element->getTags().get("amenity").toLower();
+ return amenityStr == QLatin1String("school") || amenityStr == QLatin1String("university");
+}
+
+// Schools are the only example of the concept of trying to reduce reviews between features of the
+// same type when their names indicate they are actually different types. If this concept proves
+// useful with other types, the code could be abstracted to handle them.
+
+bool PoiPolygonSchema::isSpecificSchool(const ConstElementPtr& element)
+{
+ if (!isSchool(element))
+ {
+ return false;
+ }
+ return _typeHasName("amenity=school", element->getTags().getName().toLower());
+}
+
+bool PoiPolygonSchema::specificSchoolMatch(const ConstElementPtr& element1,
+ const ConstElementPtr& element2)
+{
+ if (isSpecificSchool(element1) && isSpecificSchool(element2))
+ {
+ const QString name1 = element1->getTags().getName().toLower();
+ const QString name2 = element2->getTags().getName().toLower();
+ if (_haveMatchingTypeNames("amenity=school", name1, name2))
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool PoiPolygonSchema::isPark(const ConstElementPtr& element)
+{
+ return
+ element->getTags().get("leisure").toLower() == QLatin1String("park") &&
+ !BuildingCriterion().isSatisfied(element);
+}
+
+bool PoiPolygonSchema::isParkish(const ConstElementPtr& element)
+{
+ const QString leisureVal = element->getTags().get("leisure").toLower();
+ return
+ (leisureVal == QLatin1String("garden") || leisureVal == QLatin1String("dog_park")) &&
+ !BuildingCriterion().isSatisfied(element);
+}
+
+bool PoiPolygonSchema::isPlayground(const ConstElementPtr& element)
+{
+ return element->getTags().get("leisure").toLower() == QLatin1String("playground");
+}
+
+bool PoiPolygonSchema::isSport(const ConstElementPtr& element)
+{
+ const Tags& tags = element->getTags();
+ if (tags.contains("sport"))
+ {
+ return true;
+ }
+ const QString leisureVal = tags.get("leisure").toLower();
+ return leisureVal.contains("sport") || leisureVal == QLatin1String("pitch");
+}
+
+bool PoiPolygonSchema::isRestroom(const ConstElementPtr& element)
+{
+ return element->getTags().get("amenity").toLower() == QLatin1String("toilets");
+}
+
+bool PoiPolygonSchema::isParking(const ConstElementPtr& element)
+{
+ const Tags& tags = element->getTags();
+ if (tags.contains("parking"))
+ {
+ return true;
+ }
+ const QString amenityVal = tags.get("amenity").toLower();
+ return amenityVal == QLatin1String("parking") || amenityVal == QLatin1String("bicycle_parking");
+}
+
+bool PoiPolygonSchema::isReligion(const ConstElementPtr& element)
+{
+ const Tags& tags = element->getTags();
+ const QString amenityVal = tags.get("amenity").toLower();
+ const QString buildingVal = tags.get("building").toLower();
+ return amenityVal == QLatin1String("place_of_worship") ||
+ buildingVal == QLatin1String("church") ||
+ buildingVal == QLatin1String("mosque") ||
+ // TODO: this one is an alias of building=mosque, so we should be getting it from there
+ //instead
+ amenityVal == QLatin1String("mosque") ||
+ buildingVal == QLatin1String("synagogue");
+}
+
+bool PoiPolygonSchema::hasMoreThanOneType(const ConstElementPtr& element)
+{
+ int typeCount = 0;
+ QStringList typesParsed;
+ if (_allTypeKeys.size() == 0)
+ {
+ _allTypeKeys = OsmSchema::getInstance().getAllTypeKeys();
+ }
+
+ const Tags elementTags = element->getTags();
+ for (Tags::const_iterator it = elementTags.begin(); it != elementTags.end(); ++it)
+ {
+ const QString elementTagKey = it.key();
+ //there may be duplicate keys in allTags
+ if (_allTypeKeys.contains(elementTagKey) && !typesParsed.contains(elementTagKey))
+ {
+ LOG_TRACE("Has key: " << elementTagKey);
+ typeCount++;
+ if (typeCount > 1)
+ {
+ return true;
+ }
+ }
+
+ typesParsed.append(elementTagKey);
+ }
+ return false;
+}
+
+bool PoiPolygonSchema::hasRelatedType(const ConstElementPtr& element)
+{
+ return
+ OsmSchema::getInstance().getCategories(element->getTags()).intersects(
+ OsmSchemaCategory::building() | OsmSchemaCategory::poi());
+}
+
+bool PoiPolygonSchema::hasSpecificType(const ConstElementPtr& element)
+{
+ const Tags& tags = element->getTags();
+ return
+ !tags.contains("poi") && tags.get("building").toLower() != QLatin1String("yes") &&
+ tags.get("office").toLower() != QLatin1String("yes") &&
+ tags.get("area").toLower() != QLatin1String("yes") && hasRelatedType(element);
+}
+
+bool PoiPolygonSchema::isRestaurant(const ConstElementPtr& element)
+{
+ const Tags& tags = element->getTags();
+ const QString amenityVal = tags.get("amenity").toLower();
+ return amenityVal == QLatin1String("restaurant") || amenityVal == QLatin1String("fast_food");
+}
+
+bool PoiPolygonSchema::isNatural(const ConstElementPtr& element)
+{
+ return element->getTags().contains("natural");
+}
+
+}
+