diff --git a/coding/map_uint32_to_val.hpp b/coding/map_uint32_to_val.hpp index df331ad932..a2c4b23f80 100644 --- a/coding/map_uint32_to_val.hpp +++ b/coding/map_uint32_to_val.hpp @@ -12,6 +12,7 @@ #include "base/assert.hpp" #include "base/checked_cast.hpp" #include "base/logging.hpp" +#include "base/macros.hpp" #if defined(__clang__) #pragma clang diagnostic push @@ -33,6 +34,10 @@ #include #include +// A data structure that allows storing a map from small 32-bit integers (the main use +// case is feature ids of a single mwm) to arbitrary values and accessing this map +// with a small RAM footprint. +// // Format: // File offset (bytes) Field name Field size (bytes) // 0 version 2 @@ -44,24 +49,23 @@ // positions offset positions table variables offset - positions offset // variables offset variables blocks end of section - variables offset // -// Version and endianness is always in stored little-endian format. 0 -// value of endianness means little endian, whereas 1 means big -// endian. +// Version and endianness are always stored in the little-endian format. +// 0 value of endianness means little-endian, whereas 1 means big-endian. // -// All offsets are in little-endian format. +// All offsets are in the little-endian format. // // Identifiers table is a bit-vector with rank-select table, where set -// bits denote that centers for the corresponding features are in +// bits denote that values for the corresponding features are in the // table. Identifiers table is stored in the native endianness. // -// Positions table is a Elias-Fano table, where each entry corresponds +// Positions table is an Elias-Fano table where each entry corresponds // to the start position of the variables block. Positions table is // stored in the native endianness. // // Variables is a sequence of blocks, where each block (with the // exception of the last one) is a sequence of kBlockSize variables // encoded by block encoding callback. - +// // On Get call kBlockSize consecutive variables are decoded and cached in RAM. template @@ -200,6 +204,20 @@ public: return table; } + template + void ForEach(Fn && fn) + { + for (uint64_t i = 0; i < m_ids.num_ones(); ++i) + { + auto const j = static_cast(m_ids.select(i)); + Value value; + bool const ok = Get(j, value); + UNUSED_VALUE(ok); + ASSERT(ok, ()); + fn(j, value); + } + } + private: bool Init() { diff --git a/coding/reader.hpp b/coding/reader.hpp index 6aa6f6101e..238e0a148b 100644 --- a/coding/reader.hpp +++ b/coding/reader.hpp @@ -245,6 +245,15 @@ public: TReader SubReader() { return SubReader(Size()); } + std::unique_ptr CreateSubReader(uint64_t size) + { + uint64_t const pos = m_pos; + Skip(size); + return m_reader.CreateSubReader(pos, size); + } + + std::unique_ptr CreateSubReader() { return CreateSubReader(Size()); } + private: bool AssertPosition() const { diff --git a/coding/succinct_mapper.hpp b/coding/succinct_mapper.hpp index c99c6a223d..cacae80143 100644 --- a/coding/succinct_mapper.hpp +++ b/coding/succinct_mapper.hpp @@ -44,6 +44,13 @@ void WritePadding(TWriter & writer, uint64_t & bytesWritten) bytesWritten += padding; } +template +void SkipPadding(Source & src) +{ + uint32_t const padding = ToAlign8(src.Pos()); + src.Skip(padding); +} + class MapVisitor { public: diff --git a/generator/feature_generator.cpp b/generator/feature_generator.cpp index ea8881e396..381977003e 100644 --- a/generator/feature_generator.cpp +++ b/generator/feature_generator.cpp @@ -140,4 +140,4 @@ uint32_t CheckedFilePosCast(FileWriter const & f) ("Feature offset is out of 32bit boundary!")); return static_cast(pos); } -} +} // namespace feature diff --git a/generator/feature_generator.hpp b/generator/feature_generator.hpp index 09c535c126..f91f17e698 100644 --- a/generator/feature_generator.hpp +++ b/generator/feature_generator.hpp @@ -69,4 +69,4 @@ public: }; uint32_t CheckedFilePosCast(FileWriter const & f); -} +} // namespace feature diff --git a/indexer/data_header.hpp b/indexer/data_header.hpp index 87b074fb68..850f9a3074 100644 --- a/indexer/data_header.hpp +++ b/indexer/data_header.hpp @@ -22,16 +22,6 @@ namespace feature /// Max possible scales. @see arrays in feature_impl.hpp static const size_t MAX_SCALES_COUNT = 4; - private: - serial::GeometryCodingParams m_codingParams; - - // Rect around region border. Features which cross region border may cross this rect. - std::pair m_bounds; - - buffer_vector m_scales; - buffer_vector m_langs; - - public: DataHeader() = default; explicit DataHeader(string const & fileName); explicit DataHeader(FilesContainerR const & cont); @@ -40,10 +30,12 @@ namespace feature { m_codingParams = cp; } + inline serial::GeometryCodingParams const & GetDefGeometryCodingParams() const { return m_codingParams; } + serial::GeometryCodingParams GetGeometryCodingParams(int scaleIndex) const; m2::RectD const GetBounds() const; @@ -71,7 +63,6 @@ namespace feature inline bool IsMWMSuitable() const { return m_format <= version::Format::lastFormat; } /// @name Serialization - //@{ void Save(FileWriter & w) const; void Load(FilesContainerR const & cont); @@ -86,13 +77,20 @@ namespace feature inline MapType GetType() const { return m_type; } private: - version::Format m_format; - MapType m_type; - /// Use lastFormat as a default value for indexes building. /// Pass the valid format from wmw in all other cases. void Load(ModelReaderPtr const & r, version::Format format); void LoadV1(ModelReaderPtr const & r); - //@} + + version::Format m_format = version::Format::unknownFormat; + MapType m_type = world; + + serial::GeometryCodingParams m_codingParams; + + // Rect around region border. Features which cross region border may cross this rect. + std::pair m_bounds; + + buffer_vector m_scales; + buffer_vector m_langs; }; } diff --git a/indexer/feature_to_osm.cpp b/indexer/feature_to_osm.cpp index 969c0216fd..ab310d85f7 100644 --- a/indexer/feature_to_osm.cpp +++ b/indexer/feature_to_osm.cpp @@ -13,7 +13,7 @@ namespace indexer { FeatureIdToGeoObjectIdBimap::FeatureIdToGeoObjectIdBimap(DataSource const & dataSource) - : m_dataSource(dataSource) + : m_dataSource(dataSource), m_reader(std::unique_ptr()) { } @@ -37,11 +37,11 @@ bool FeatureIdToGeoObjectIdBimap::Load() return false; } + bool success = false; try { - auto reader = cont.GetReader(FEATURE_TO_OSM_FILE_TAG); - ReaderSource> source(reader); - FeatureIdToGeoObjectIdSerDes::Deserialize(source, m_map); + m_reader = cont.GetReader(FEATURE_TO_OSM_FILE_TAG); + success = FeatureIdToGeoObjectIdSerDes::Deserialize(*m_reader.GetPtr(), *this); } catch (Reader::Exception const & e) { @@ -49,8 +49,13 @@ bool FeatureIdToGeoObjectIdBimap::Load() return false; } - m_mwmId = handle.GetId(); - return true; + if (success) + { + m_mwmId = handle.GetId(); + return true; + } + + return false; } bool FeatureIdToGeoObjectIdBimap::GetGeoObjectId(FeatureID const & fid, base::GeoObjectId & id) @@ -61,16 +66,65 @@ bool FeatureIdToGeoObjectIdBimap::GetGeoObjectId(FeatureID const & fid, base::Ge return false; } - return m_map.GetValue(fid.m_index, id); + if (m_memAll != nullptr) + return m_memAll->GetValue(fid.m_index, id); + + if (m_mapNodes == nullptr || m_mapWays == nullptr || m_mapRelations == nullptr) + return false; + + uint64_t serialId; + if (m_mapNodes->Get(fid.m_index, serialId)) + { + id = base::MakeOsmNode(serialId); + return true; + } + + if (m_mapWays->Get(fid.m_index, serialId)) + { + id = base::MakeOsmWay(serialId); + return true; + } + + if (m_mapRelations->Get(fid.m_index, serialId)) + { + id = base::MakeOsmRelation(serialId); + return true; + } + + return false; } bool FeatureIdToGeoObjectIdBimap::GetFeatureID(base::GeoObjectId const & id, FeatureID & fid) { + LOG(LWARNING, ("Getting GeoObjectId by FeatureId uses a lot of RAM in current implementation")); + + if (!m_mwmId.IsAlive()) + return false; + + if (m_memAll == nullptr) + { + m_memAll = std::make_unique(); + bool const success = FeatureIdToGeoObjectIdSerDes::Deserialize(*m_reader.GetPtr(), *m_memAll); + if (!success) + { + m_memAll.reset(); + LOG(LERROR, ("Could not deserialize the FeatureId to OsmId mapping into memory.")); + return false; + } + } + uint32_t index; - if (!m_map.GetKey(id, index)) + if (!m_memAll->GetKey(id, index)) return false; fid = FeatureID(m_mwmId, index); return true; } + +// static +std::string const FeatureIdToGeoObjectIdSerDes::kHeaderMagic = "mwmftosm"; +FeatureIdToGeoObjectIdSerDes::Version const FeatureIdToGeoObjectIdSerDes::kLatestVersion = + FeatureIdToGeoObjectIdSerDes::Version::V0; +size_t constexpr FeatureIdToGeoObjectIdSerDes::kMagicAndVersionSize = 9; +size_t constexpr FeatureIdToGeoObjectIdSerDes::kHeaderOffset = 16; } // namespace indexer diff --git a/indexer/feature_to_osm.hpp b/indexer/feature_to_osm.hpp index d6b50e4128..67cacdabbd 100644 --- a/indexer/feature_to_osm.hpp +++ b/indexer/feature_to_osm.hpp @@ -3,14 +3,26 @@ #include "indexer/data_source.hpp" #include "indexer/feature_decl.hpp" +#include "coding/file_container.hpp" +#include "coding/map_uint32_to_val.hpp" +#include "coding/reader.hpp" +#include "coding/succinct_mapper.hpp" #include "coding/varint.hpp" +#include "coding/write_to_sink.hpp" +#include "coding/writer.hpp" +#include "base/assert.hpp" #include "base/bidirectional_map.hpp" #include "base/checked_cast.hpp" #include "base/geo_object_id.hpp" +#include "base/logging.hpp" +#include +#include +#include #include #include +#include class DataSource; @@ -25,54 +37,308 @@ using FeatureIdToGeoObjectIdBimapMem = base::BidirectionalMap void ForEachEntry(Fn && fn) const { - m_map.ForEachEntry(std::forward(fn)); + if (!m_mwmId.IsAlive()) + return; + + if (m_mapNodes != nullptr) + { + m_mapNodes->ForEach( + [&](uint32_t fid, uint64_t serialId) { fn(fid, base::MakeOsmNode(serialId)); }); + } + if (m_mapWays != nullptr) + { + m_mapWays->ForEach( + [&](uint32_t fid, uint64_t serialId) { fn(fid, base::MakeOsmWay(serialId)); }); + } + if (m_mapRelations != nullptr) + { + m_mapRelations->ForEach( + [&](uint32_t fid, uint64_t serialId) { fn(fid, base::MakeOsmRelation(serialId)); }); + } } private: DataSource const & m_dataSource; MwmSet::MwmId m_mwmId; + FilesContainerR::TReader m_reader; - FeatureIdToGeoObjectIdBimapMem m_map; + std::unique_ptr m_memAll; + + std::unique_ptr> m_mapNodes; + std::unique_ptr> m_mapWays; + std::unique_ptr> m_mapRelations; + + std::unique_ptr m_nodesReader; + std::unique_ptr m_waysReader; + std::unique_ptr m_relationsReader; }; +// Section format. +// +// Versions supported Name Offset in bytes Size in bytes +// all magic (to ease the debugging) 0 8 +// all version 8 1 +// v0 header_v0 16 20 +// v0 fid -> osm nodes mapping saved in header_v0 +// v0 fid -> osm ways mapping saved in header_v0 +// v0 fid -> osm relations mapping saved in header_v0 +// +// All offsets are aligned to 8 bytes. +// All integer values larger than a byte are little-endian. class FeatureIdToGeoObjectIdSerDes { public: + enum class Version : uint8_t + { + V0, + }; + + static std::string const kHeaderMagic; + static Version const kLatestVersion; + static size_t const kMagicAndVersionSize; + static size_t const kHeaderOffset; + static std::array kTypes; + + struct HeaderV0 + { + template + void Write(Sink & sink) + { + WriteToSink(sink, m_numEntries); + + WriteToSink(sink, m_nodesOffset); + WriteToSink(sink, m_waysOffset); + WriteToSink(sink, m_relationsOffset); + WriteToSink(sink, m_typesSentinelOffset); + } + + template + void Read(Source & src) + { + m_numEntries = ReadPrimitiveFromSource(src); + + m_nodesOffset = ReadPrimitiveFromSource(src); + m_waysOffset = ReadPrimitiveFromSource(src); + m_relationsOffset = ReadPrimitiveFromSource(src); + m_typesSentinelOffset = ReadPrimitiveFromSource(src); + } + + uint32_t m_numEntries = 0; + + // All offsets are relative to the start of the payload (which may differ + // from the end of the header because of padding). + uint32_t m_nodesOffset = 0; + uint32_t m_waysOffset = 0; + uint32_t m_relationsOffset = 0; + uint32_t m_typesSentinelOffset = 0; + }; + + static_assert(sizeof(HeaderV0) == 20, ""); + template static void Serialize(Sink & sink, FeatureIdToGeoObjectIdBimapMem const & map) { - WriteToSink(sink, base::checked_cast(map.Size())); - map.ForEachEntry([&sink](uint32_t const fid, base::GeoObjectId gid) { - WriteVarUint(sink, fid); - WriteVarUint(sink, gid.GetEncodedId()); + auto const startPos = sink.Pos(); + sink.Write(kHeaderMagic.data(), kHeaderMagic.size()); + WriteToSink(sink, static_cast(kLatestVersion)); + CHECK_EQUAL(sink.Pos() - startPos, kMagicAndVersionSize, ()); + WriteZeroesToSink(sink, kHeaderOffset - kMagicAndVersionSize); + CHECK_EQUAL(sink.Pos() - startPos, kHeaderOffset, ()); + + switch (kLatestVersion) + { + case Version::V0: + { + HeaderV0 header; + header.Write(sink); + EnsurePadding(sink, startPos); + + SerializeV0(sink, map, header); + + auto savedPos = sink.Pos(); + sink.Seek(startPos + kHeaderOffset); + header.Write(sink); + sink.Seek(savedPos); + } + return; + default: UNREACHABLE(); + } + } + + template + static bool Deserialize(Reader & reader, Map & map) + { + ReaderSource src(reader); + + if (src.Size() < kHeaderOffset) + { + LOG(LINFO, ("Unable to deserialize FeatureToOsm map: wrong header magic or version")); + return false; + } + std::string magic(kHeaderMagic.size(), '\0'); + src.Read(&magic[0], magic.size()); + if (magic != kHeaderMagic) + { + LOG(LINFO, ("Unable to deserialize FeatureToOsm map: wrong header magic:", magic)); + return false; + } + + auto const version = static_cast(ReadPrimitiveFromSource(src)); + src.Skip(kHeaderOffset - kMagicAndVersionSize); + + switch (version) + { + case Version::V0: + { + HeaderV0 header; + header.Read(src); + coding::SkipPadding(src); + DeserializeV0(*src.CreateSubReader(), header, map); + } + return true; + default: LOG(LINFO, ("Unable to deserialize FeatureToOsm map: unknown version")); return false; + } + } + + template + static void SerializeV0(Sink & sink, FeatureIdToGeoObjectIdBimapMem const & map, + HeaderV0 & header) + { + using Type = base::GeoObjectId::Type; + auto const startPos = base::checked_cast(sink.Pos()); + SerializeV0(sink, Type::OsmNode, header.m_nodesOffset, map); + SerializeV0(sink, Type::OsmWay, header.m_waysOffset, map); + SerializeV0(sink, Type::OsmRelation, header.m_relationsOffset, map); + + header.m_numEntries = static_cast(map.Size()); + + header.m_nodesOffset -= startPos; + header.m_waysOffset -= startPos; + header.m_relationsOffset -= startPos; + header.m_typesSentinelOffset = base::checked_cast(sink.Pos()) - startPos; + } + + template + static void SerializeV0(Sink & sink, base::GeoObjectId::Type type, uint32_t & offset, + FeatureIdToGeoObjectIdBimapMem const & map) + { + offset = base::checked_cast(sink.Pos()); + std::vector> entries; + entries.reserve(map.Size()); + type = NormalizedType(type); + map.ForEachEntry([&sink, &entries, type](uint32_t const fid, base::GeoObjectId gid) { + if (NormalizedType(gid.GetType()) == type) + entries.emplace_back(fid, gid); + }); + + std::sort(entries.begin(), entries.end()); + + MapUint32ToValueBuilder builder; + + for (auto const & entry : entries) + builder.Put(entry.first, entry.second.GetSerialId()); + + builder.Freeze(sink, WriteBlockCallback); + + EnsurePadding(sink, offset); + } + + template + static void DeserializeV0(Reader & reader, HeaderV0 const & header, + FeatureIdToGeoObjectIdBimap & map) + { + auto const nodesSize = header.m_waysOffset - header.m_nodesOffset; + auto const waysSize = header.m_relationsOffset - header.m_waysOffset; + auto const relationsSize = header.m_typesSentinelOffset - header.m_relationsOffset; + + map.m_nodesReader = reader.CreateSubReader(header.m_nodesOffset, nodesSize); + map.m_waysReader = reader.CreateSubReader(header.m_waysOffset, waysSize); + map.m_relationsReader = reader.CreateSubReader(header.m_relationsOffset, relationsSize); + + map.m_mapNodes = MapUint32ToValue::Load(*map.m_nodesReader, ReadBlockCallback); + map.m_mapWays = MapUint32ToValue::Load(*map.m_waysReader, ReadBlockCallback); + map.m_mapRelations = + MapUint32ToValue::Load(*map.m_relationsReader, ReadBlockCallback); + } + + template + static void DeserializeV0(Reader & reader, HeaderV0 const & header, + FeatureIdToGeoObjectIdBimapMem & memMap) + { + using Type = base::GeoObjectId::Type; + + memMap.Clear(); + + auto const nodesSize = header.m_waysOffset - header.m_nodesOffset; + auto const waysSize = header.m_relationsOffset - header.m_waysOffset; + auto const relationsSize = header.m_typesSentinelOffset - header.m_relationsOffset; + + DeserializeV0ToMem(*reader.CreateSubReader(header.m_nodesOffset, nodesSize), + Type::ObsoleteOsmNode, memMap); + DeserializeV0ToMem(*reader.CreateSubReader(header.m_waysOffset, waysSize), Type::ObsoleteOsmWay, + memMap); + DeserializeV0ToMem(*reader.CreateSubReader(header.m_relationsOffset, relationsSize), + Type::ObsoleteOsmRelation, memMap); + } + + template + static void DeserializeV0ToMem(Reader & reader, base::GeoObjectId::Type type, + FeatureIdToGeoObjectIdBimapMem & memMap) + { + auto const map = MapUint32ToValue::Load(reader, ReadBlockCallback); + CHECK(map, ()); + map->ForEach([&](uint32_t fid, uint64_t & serialId) { + memMap.Add(fid, base::GeoObjectId(type, serialId)); }); } - template - static void Deserialize(Source & src, FeatureIdToGeoObjectIdBimapMem & map) +private: + template + static void EnsurePadding(Sink & sink, uint64_t startPos) { - map.Clear(); - auto const numEntries = ReadPrimitiveFromSource(src); - for (size_t i = 0; i < numEntries; ++i) + uint64_t bytesWritten = sink.Pos() - startPos; + coding::WritePadding(sink, bytesWritten); + } + + static base::GeoObjectId::Type NormalizedType(base::GeoObjectId::Type type) + { + using Type = base::GeoObjectId::Type; + switch (type) { - auto const fid = ReadVarUint(src); - auto const gid = ReadVarUint(src); - map.Add(fid, base::GeoObjectId(gid)); + case Type::ObsoleteOsmNode: return Type::OsmNode; + case Type::ObsoleteOsmWay: return Type::OsmWay; + case Type::ObsoleteOsmRelation: return Type::OsmRelation; + default: return type; } } + + static void ReadBlockCallback(NonOwningReaderSource & src, uint32_t blockSize, + std::vector & values) + { + values.clear(); + values.reserve(blockSize); + for (size_t i = 0; i < blockSize && src.Size() > 0; ++i) + values.emplace_back(ReadVarUint(src)); + } + + static void WriteBlockCallback(Writer & writer, std::vector::const_iterator begin, + std::vector::const_iterator end) + { + for (auto it = begin; it != end; ++it) + WriteVarUint(writer, *it); + } }; } // namespace indexer diff --git a/indexer/indexer_tests/feature_to_osm_tests.cpp b/indexer/indexer_tests/feature_to_osm_tests.cpp index ed8efeec20..918ceaeba7 100644 --- a/indexer/indexer_tests/feature_to_osm_tests.cpp +++ b/indexer/indexer_tests/feature_to_osm_tests.cpp @@ -1,10 +1,11 @@ #include "testing/testing.hpp" +#include "generator/generator_tests_support/test_with_custom_mwms.hpp" + #include "indexer/feature_decl.hpp" #include "indexer/feature_to_osm.hpp" -#include "platform/mwm_version.hpp" - +#include "coding/file_writer.hpp" #include "coding/reader.hpp" #include "coding/writer.hpp" @@ -18,8 +19,11 @@ #include #include +#include "defines.hpp" + using namespace indexer; using namespace std; +using namespace generator::tests_support; namespace { @@ -35,14 +39,29 @@ Entries GetEntries(Cont const & cont) sort(res.begin(), res.end()); return res; }; + +class FeatureIdToGeoObjectIdBimapTest : public TestWithCustomMwms +{ +public: + DataSource const & GetDataSource() const { return m_dataSource; } +}; } // namespace -UNIT_TEST(FeatureIdToGeoObjectIdBimap_Smoke) +UNIT_CLASS_TEST(FeatureIdToGeoObjectIdBimapTest, Smoke) { - FrozenDataSource dataSource; + Entries const kEntries = { + {0, base::MakeOsmNode(123)}, + {1, base::MakeOsmWay(456)}, + {2, base::MakeOsmRelation(789)}, + }; FeatureIdToGeoObjectIdBimapMem origM; - origM.Add(0, base::MakeOsmWay(123)); + for (auto const & e : kEntries) + origM.Add(e.first, e.second); + + // TestMwmBuilder will create the section but we will rewrite it right away. + auto testWorldId = BuildWorld([&](TestMwmBuilder & builder) {}); + auto const testWorldPath = testWorldId.GetInfo()->GetLocalFile().GetPath(MapOptions::Map); vector buf; { @@ -50,15 +69,36 @@ UNIT_TEST(FeatureIdToGeoObjectIdBimap_Smoke) FeatureIdToGeoObjectIdSerDes::Serialize(writer, origM); } - FeatureIdToGeoObjectIdBimapMem deserM; { - MemReader reader(buf.data(), buf.size()); - ReaderSource src(reader); - FeatureIdToGeoObjectIdSerDes::Deserialize(src, deserM); + FilesContainerW writer(testWorldPath, FileWriter::OP_WRITE_EXISTING); + writer.Write(buf, FEATURE_TO_OSM_FILE_TAG); } - Entries expectedEntries = GetEntries(origM); + FeatureIdToGeoObjectIdBimapMem deserMem; + { + MemReader reader(buf.data(), buf.size()); + FeatureIdToGeoObjectIdSerDes::Deserialize(reader, deserMem); + } + + indexer::FeatureIdToGeoObjectIdBimap deserM(GetDataSource()); + TEST(deserM.Load(), ()); + + Entries actualEntriesMem = GetEntries(deserMem); Entries actualEntries = GetEntries(deserM); - TEST(!expectedEntries.empty(), ()); - TEST_EQUAL(expectedEntries, actualEntries, ()); + TEST_EQUAL(kEntries, actualEntriesMem, ()); + TEST_EQUAL(kEntries, actualEntries, ()); + + for (auto const & entry : kEntries) + { + base::GeoObjectId gid; + TEST(deserM.GetGeoObjectId(FeatureID(testWorldId, entry.first), gid), ()); + TEST_EQUAL(entry.second, gid, ()); + } + + for (auto const & entry : kEntries) + { + FeatureID fid; + TEST(deserM.GetFeatureID(entry.second, fid), ()); + TEST_EQUAL(entry.first, fid.m_index, ()); + } } diff --git a/platform/local_country_file.hpp b/platform/local_country_file.hpp index 0a1b73ae86..c64f7808e2 100644 --- a/platform/local_country_file.hpp +++ b/platform/local_country_file.hpp @@ -31,7 +31,7 @@ public: // Creates an instance holding a path to countryFile's in a // directory. Note that no disk operations are not performed until // SyncWithDisk() is called. - // The directory must containt a full path to the country file. + // The directory must contain a full path to the country file. LocalCountryFile(string const & directory, CountryFile const & countryFile, int64_t version); // Syncs internal state like availability of map and routing files,