diff --git a/.dockerignore b/.dockerignore
new file mode 100755
index 0000000..d33e9ab
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+.vscode
+src/node_modules
+src/wwwroot/
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100755
index 0000000..3e3d800
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.env
+src/node_modules
+src/wwwroot/
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100755
index 0000000..125fe6d
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,34 @@
+# Server Build
+FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS server
+WORKDIR /work/src
+
+COPY ./src/Aptabase.csproj /work/src
+
+ARG TARGETARCH
+RUN dotnet restore "./Aptabase.csproj" -a $TARGETARCH
+
+COPY ./etc/clickhouse /work/etc/clickhouse
+COPY ./etc/geoip /work/etc/geoip
+COPY ./src /work/src
+
+RUN dotnet publish "Aptabase.csproj" -a $TARGETARCH -c Release -o /work/publish /p:UseAppHost=false
+
+# WebApp Build
+FROM oven/bun:1 AS webapp
+WORKDIR /work
+
+COPY ./src/package.json ./src/package-lock.json ./
+RUN bun install
+
+COPY ./src ./
+RUN bun run build
+
+# Final
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
+WORKDIR /app
+
+COPY --from=server /work/publish .
+COPY --from=webapp /work/wwwroot ./wwwroot
+COPY LICENSE .
+
+ENTRYPOINT ["dotnet", "Aptabase.dll"]
diff --git a/LICENSE b/LICENSE
new file mode 100755
index 0000000..f4aa842
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,619 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
\ No newline at end of file
diff --git a/README.md b/README.md
old mode 100644
new mode 100755
index dc18a21..cdf4897
--- a/README.md
+++ b/README.md
@@ -1 +1,56 @@
-# Zalvena
+# Zalvena Service
+
+A fork of Aptabase Server software, modified to only work with projects using Zalvena.
+
+## Changes
+ - Updated app key generation to use `ZV` instead of `SH`
+ - Updated browserslist
+ - Updated `node:18` to `over/bun:1` in `Dockerfile`
+ - Updated the pacakge `@types/node` from `20.8.7` to `22.13.4`
+ - Updated the pacakge `@types/react` from `18.2.30` to `19.0.10`
+ - Updated the pacakge `@types/react-dom` from `18.2.14` to `19.0.4`
+ - Updated the pacakge `@vitejs/plugin-react-swc` from `3.4.0` to `3.8.0`
+ - Updated the pacakge `autoprefixer` from `10.4.16` to `10.4.20`
+ - Updated the pacakge `postcss` from `8.4.31` to `8.5.3`
+ - Updated the pacakge `tailwind-merge` from `1.14.0` to `3.0.1`
+ - Updated the pacakge `typescript` from `5.2.2` to `5.7.3`
+ - Updated the pacakge `vite` from `4.5.3` to `6.1.1`
+ - Updated the pacakge `vite-plugin-mkcert` from `1.17.5` to `1.17.6`
+ - Updated the pacakge `@aptabase/web` from `0.4.0` to `0.4.3`
+ - Updated the pacakge `@fontsource/inter` from `5.0.14` to `5.1.1`
+ - Updated the pacakge `@headlessui/react` from `2.1.2` to `2.2.0`
+ - Updated the pacakge `@radix-ui/react-accordion` from `1.1.2` to `1.2.3`
+ - Updated the pacakge `@radix-ui/react-label` from `2.0.2` to `2.1.2`
+ - Updated the pacakge `@radix-ui/react-popover` from `1.0.7` to `1.1.6`
+ - Updated the pacakge `@radix-ui/react-select` from `2.0.0` to `2.1.6`
+ - Updated the pacakge `@radix-ui/react-slot` from `1.0.2` to `1.1.2`
+ - Updated the pacakge `@radix-ui/react-tabs` from `1.0.4` to `1.1.3`
+ - Updated the pacakge `@radix-ui/react-tooltip` from `1.0.7` to `1.1.8`
+ - Updated the pacakge `@tabler/icons-react` from `2.39.0` to `3.30.0`
+ - Updated the pacakge `@tanstack/react-query` from `5.0.0` to `5.66.7`
+ - Updated the pacakge `chart.js` from `4.4.0` to `4.4.8`
+ - Updated the pacakge `chartjs-plugin-annotation` from `3.0.1` to `3.1.0`
+ - Updated the pacakge `cmdk` from `0.2.1` to `1.0.4`
+ - Updated the pacakge `date-fns` from `3.6.0` to `4.1.0`
+ - Updated the pacakge `framer-motion` from `10.16.4` to `12.4.5`
+ - Updated the pacakge `jotai` from `2.9.1` to `2.12.1`
+ - Updated the pacakge `react` from `18.2.0` to `19.0.0`
+ - Updated the pacakge `react-dom` from `18.2.0` to `19.0.0`
+ - Updated the pacakge `react-markdown` from `9.0.0` to `9.0.3`
+ - Updated the pacakge `react-router-dom` from `6.17.0` to `7.2.0`
+ - Updated the pacakge `rehype-highlight` from `7.0.0` to `7.0.2`
+ - Updated the pacakge `remark-gfm` from `4.0.0` to `4.0.1`
+ - Updated the pacakge `sonner` from `1.0.3` to `2.0.0`
+ - Remote sources removed to make this fork comply with SudoVanilla Umbrella Policy
+ - Default avatar points to https://md.sudovanilla.org/images/icons/Aptabase.jpg
+ - Flags point to https://md.sudovanilla.org/images/flags/
+ - Removed all support elements and Chirp
+ - Removed instructions screen
+ - Removed `/etc/tinybird`
+ - Removed `/src/Properties/`
+ - Removed `/tests/`
+ - Removed `/tools/`
+
+## License
+
+Aptabase and Zalvena Service is open-source under the [AGPLv3 license](./LICENSE). You can use it for free, but you must share any changes you make to the code.
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100755
index 0000000..f41d5ad
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,36 @@
+services:
+ aptabase_db:
+ container_name: aptabase_db
+ image: postgres:15-alpine
+ restart: always
+ volumes:
+ - ./tmp/db-data:/var/lib/postgresql/data
+ environment:
+ POSTGRES_USER: aptabase
+ POSTGRES_PASSWORD: sLPnqXzjupt6pR1dumYrGeVXpVDup49hYRRn2TEUfRmF6eXbBu4CJYl5ynBFxXx8
+ aptabase_events_db:
+ container_name: aptabase_events_db
+ image: clickhouse/clickhouse-server:23.8.4.69-alpine
+ restart: always
+ volumes:
+ - ./tmp/events-db-data:/var/lib/clickhouse
+ environment:
+ CLICKHOUSE_USER: aptabase
+ CLICKHOUSE_PASSWORD: lBxMObwAWOU8L4ieQPq1ZcbQeUND4FwwgEfvmGstU5o8IZS3XekInmKtcAwqSxgm
+ ulimits:
+ nofile:
+ soft: 262144
+ hard: 262144
+ aptabase:
+ container_name: aptabase_app
+ image: oci.registry.sudovanilla.org/zalvena
+ restart: always
+ env_file:
+ - ./.env
+ volumes:
+ - /etc/hosts:/etc/hosts
+ depends_on:
+ - aptabase_events_db
+ - aptabase_db
+ ports:
+ - 6521:8080
diff --git a/etc/clickhouse/0001-events.sql b/etc/clickhouse/0001-events.sql
new file mode 100755
index 0000000..a670942
--- /dev/null
+++ b/etc/clickhouse/0001-events.sql
@@ -0,0 +1,26 @@
+CREATE TABLE IF NOT EXISTS events
+(
+ `app_id` String,
+ `timestamp` DateTime,
+ `event_name` String,
+ `user_id` String,
+ `session_id` String,
+ `os_name` LowCardinality(String),
+ `os_version` String,
+ `locale` LowCardinality(String),
+ `app_version` String,
+ `app_build_number` String,
+ `engine_name` LowCardinality(String),
+ `engine_version` String,
+ `sdk_version` String,
+ `country_code` LowCardinality(String),
+ `region_name` LowCardinality(String),
+ `city` String,
+ `string_props` String,
+ `numeric_props` String,
+ `ttl` DateTime
+)
+ENGINE = MergeTree()
+PARTITION BY toYYYYMM(timestamp)
+ORDER BY (app_id, timestamp, event_name)
+TTL ttl;
\ No newline at end of file
diff --git a/etc/clickhouse/0002-monthly_usage_v1_mv.sql b/etc/clickhouse/0002-monthly_usage_v1_mv.sql
new file mode 100755
index 0000000..e6beb85
--- /dev/null
+++ b/etc/clickhouse/0002-monthly_usage_v1_mv.sql
@@ -0,0 +1,8 @@
+CREATE OR REPLACE VIEW monthly_usage_v1
+AS
+SELECT
+ app_id,
+ toStartOfMonth(timestamp) AS period,
+ countState() AS events
+FROM events
+GROUP BY app_id, period
\ No newline at end of file
diff --git a/etc/clickhouse/0003-sessions_live_v1.sql b/etc/clickhouse/0003-sessions_live_v1.sql
new file mode 100755
index 0000000..b05a821
--- /dev/null
+++ b/etc/clickhouse/0003-sessions_live_v1.sql
@@ -0,0 +1,28 @@
+CREATE OR REPLACE VIEW sessions_live_v1
+AS
+SELECT
+ app_id,
+ session_id,
+ user_id,
+ minSimpleState(e.timestamp) AS min_timestamp,
+ maxSimpleState(e.timestamp) AS max_timestamp,
+ anySimpleState(os_name) AS os_name,
+ anySimpleState(os_version) AS os_version,
+ anySimpleState(locale) AS locale,
+ anySimpleState(app_version) AS app_version,
+ anySimpleState(app_build_number) AS app_build_number,
+ anySimpleState(engine_name) AS engine_name,
+ anySimpleState(engine_version) AS engine_version,
+ anySimpleState(sdk_version) AS sdk_version,
+ anySimpleState(country_code) AS country_code,
+ anySimpleState(region_name) AS region_name,
+ countState() AS events_count,
+ groupArrayState(event_name) AS events_name,
+ groupArrayState(e.timestamp) AS events_timestamp,
+ groupArrayState(string_props) AS events_string_props,
+ groupArrayState(numeric_props) AS events_numeric_props
+FROM events AS e
+GROUP BY
+ app_id,
+ session_id,
+ user_id
\ No newline at end of file
diff --git a/etc/clickhouse/0004-add_device_model_to_events.sql b/etc/clickhouse/0004-add_device_model_to_events.sql
new file mode 100755
index 0000000..09938b4
--- /dev/null
+++ b/etc/clickhouse/0004-add_device_model_to_events.sql
@@ -0,0 +1,2 @@
+ALTER TABLE events
+ADD COLUMN IF NOT EXISTS device_model String;
\ No newline at end of file
diff --git a/etc/clickhouse/queries/billing_historical_usage__v1.liquid b/etc/clickhouse/queries/billing_historical_usage__v1.liquid
new file mode 100755
index 0000000..5b9c74e
--- /dev/null
+++ b/etc/clickhouse/queries/billing_historical_usage__v1.liquid
@@ -0,0 +1,7 @@
+SELECT period as Date,
+ countMerge(events) as Events
+FROM monthly_usage_v1
+WHERE app_id IN ('{{app_ids}}')
+AND period >= toStartOfMonth(now() - INTERVAL 6 MONTH)
+GROUP BY period
+ORDER BY period WITH FILL FROM toStartOfMonth(now() - INTERVAL 6 MONTH) TO toStartOfMonth(now() + INTERVAL 1 MONTH) STEP toIntervalMonth(1)
\ No newline at end of file
diff --git a/etc/clickhouse/queries/billing_usage_per_app__v1.liquid b/etc/clickhouse/queries/billing_usage_per_app__v1.liquid
new file mode 100755
index 0000000..39c8d4f
--- /dev/null
+++ b/etc/clickhouse/queries/billing_usage_per_app__v1.liquid
@@ -0,0 +1,5 @@
+SELECT replace(app_id, '_DEBUG', '') as AppId,
+countMerge(events) as Count
+FROM monthly_usage_v1
+WHERE period = '{{period}}'
+GROUP BY replace(app_id, '_DEBUG', '')
\ No newline at end of file
diff --git a/etc/clickhouse/queries/get_billing_usage__v1.liquid b/etc/clickhouse/queries/get_billing_usage__v1.liquid
new file mode 100755
index 0000000..484d5ea
--- /dev/null
+++ b/etc/clickhouse/queries/get_billing_usage__v1.liquid
@@ -0,0 +1,4 @@
+SELECT countMerge(events) as Count
+FROM monthly_usage_v1
+WHERE app_id IN ('{{app_ids}}')
+AND period = toStartOfMonth(now())
diff --git a/etc/clickhouse/queries/historical_sessions__v1.liquid b/etc/clickhouse/queries/historical_sessions__v1.liquid
new file mode 100755
index 0000000..2419390
--- /dev/null
+++ b/etc/clickhouse/queries/historical_sessions__v1.liquid
@@ -0,0 +1,36 @@
+SELECT session_id as Id,
+ min(min_timestamp) as StartedAt,
+ max(max_timestamp) - min(min_timestamp) as Duration,
+ countMerge(events_count) as EventsCount,
+ any(app_version) as AppVersion,
+ any(country_code) as CountryCode,
+ any(region_name) as RegionName,
+ any(os_name) as OsName,
+ any(os_version) as OsVersion
+FROM sessions_live_v1
+WHERE app_id = '{{app_id}}'
+{% if country_code %}
+AND country_code = '{{country_code}}'
+{% endif %}
+{% if os_name %}
+AND os_name = '{{os_name}}'
+{% endif %}
+{% if app_version %}
+AND app_version = '{{app_version}}'
+{% endif %}
+{% if date_to %}
+AND (min_timestamp < '{{date_to}}' OR (min_timestamp = '{{date_to}}' AND session_id < '{{session_id}}'))
+{% endif %}
+{% if date_from %}
+AND (min_timestamp > '{{date_from}}' OR (min_timestamp = '{{date_from}}' AND session_id > '{{session_id}}'))
+{% endif %}
+GROUP BY session_id
+{% if event_name %}
+HAVING hasAny(groupArrayMerge(events_name), ['{{event_name}}'])
+{% endif %}
+{% if date_from %}
+ORDER BY StartedAt ASC, Id ASC
+{% else %}
+ORDER BY StartedAt DESC, Id DESC
+{% endif %}
+LIMIT 10
\ No newline at end of file
diff --git a/etc/clickhouse/queries/key_metrics__v1.liquid b/etc/clickhouse/queries/key_metrics__v1.liquid
new file mode 100755
index 0000000..38bf503
--- /dev/null
+++ b/etc/clickhouse/queries/key_metrics__v1.liquid
@@ -0,0 +1,30 @@
+SELECT uniqExact(user_id) / (date_diff('day', min(min), max(max)) + 1) as DailyUsers,
+ uniqExact(session_id) as Sessions,
+ if(isNaN(median(max - min)), 0, median(max - min)) as DurationSeconds,
+ sum(count) as Events
+FROM (
+ SELECT min(timestamp) AS min,
+ max(timestamp) AS max,
+ user_id,
+ session_id,
+ count(*) AS count
+ FROM events
+ PREWHERE app_id = '{{app_id}}'
+ WHERE 1 = 1
+ {% if date_from and date_to %}
+ AND timestamp BETWEEN '{{date_from}}' AND '{{date_to}}'
+ {% endif %}
+ {% if country_code %}
+ AND country_code = '{{country_code}}'
+ {% endif %}
+ {% if event_name %}
+ AND event_name = '{{event_name}}'
+ {% endif %}
+ {% if os_name %}
+ AND os_name = '{{os_name}}'
+ {% endif %}
+ {% if app_version %}
+ AND app_version = '{{app_version}}'
+ {% endif %}
+ GROUP BY user_id, session_id
+)
\ No newline at end of file
diff --git a/etc/clickhouse/queries/key_metrics__v2.liquid b/etc/clickhouse/queries/key_metrics__v2.liquid
new file mode 100755
index 0000000..6e0a051
--- /dev/null
+++ b/etc/clickhouse/queries/key_metrics__v2.liquid
@@ -0,0 +1,33 @@
+SELECT uniqExact(user_id) / (date_diff('day', min(min), max(max)) + if(date_diff('day', min(min), max(max)) = 0, 1, 0)) AS DailyUsers,
+ uniqExact(session_id) as Sessions,
+ if(isNaN(median(max - min)), 0, median(max - min)) as DurationSeconds,
+ sum(count) as Events
+FROM (
+ SELECT min(timestamp) AS min,
+ max(timestamp) AS max,
+ user_id,
+ session_id,
+ count(*) AS count
+ FROM events
+ PREWHERE app_id = '{{app_id}}'
+ WHERE 1 = 1
+ {% if date_from and date_to %}
+ AND timestamp BETWEEN '{{date_from}}' AND '{{date_to}}'
+ {% endif %}
+ {% if country_code %}
+ AND country_code = '{{country_code}}'
+ {% endif %}
+ {% if event_name %}
+ AND event_name = '{{event_name}}'
+ {% endif %}
+ {% if os_name %}
+ AND os_name = '{{os_name}}'
+ {% endif %}
+ {% if device_model %}
+ AND device_model = '{{device_model}}'
+ {% endif %}
+ {% if app_version %}
+ AND app_version = '{{app_version}}'
+ {% endif %}
+ GROUP BY user_id, session_id
+)
\ No newline at end of file
diff --git a/etc/clickhouse/queries/key_metrics_periodic__v1.liquid b/etc/clickhouse/queries/key_metrics_periodic__v1.liquid
new file mode 100755
index 0000000..4cc72c5
--- /dev/null
+++ b/etc/clickhouse/queries/key_metrics_periodic__v1.liquid
@@ -0,0 +1,53 @@
+SELECT
+ {% if granularity == 'Hour' %}
+ toStartOfHour(timestamp)
+ {% elsif granularity == 'Day' %}
+ toStartOfDay(timestamp)
+ {% else %}
+ toStartOfMonth(timestamp)
+ {% endif %} AS Period,
+ {% if granularity == 'Month' %}
+ uniqExact(user_id) / (date_diff('day', Period, toLastDayOfMonth(Period)) + 1)
+ {% else %}
+ uniqExact(user_id)
+ {% endif %} AS Users,
+ uniqExact(session_id) AS Sessions,
+ count() AS Events
+FROM events
+PREWHERE app_id = '{{app_id}}'
+WHERE 1 = 1
+{% if date_from and date_to %}
+AND timestamp BETWEEN '{{date_from}}' AND '{{date_to}}'
+{% endif %}
+{% if country_code %}
+AND country_code = '{{country_code}}'
+{% endif %}
+{% if event_name %}
+AND event_name = '{{event_name}}'
+{% endif %}
+{% if os_name %}
+AND os_name = '{{os_name}}'
+{% endif %}
+{% if app_version %}
+AND app_version = '{{app_version}}'
+{% endif %}
+GROUP by Period
+ORDER BY Period ASC
+WITH FILL
+{% if date_from and date_to %}
+{% if granularity == 'Hour' %}
+FROM toStartOfHour(toDateTime('{{date_from}}')) TO toStartOfHour(toDateTime('{{date_to}}'))
+{% elsif granularity == 'Day' %}
+FROM toStartOfDay(toDateTime('{{date_from}}')) TO toStartOfDay(toDateTime('{{date_to}}'))
+{% else %}
+FROM toStartOfMonth(toDateTime('{{date_from}}')) TO toStartOfMonth(toDateTime('{{date_to}}'))
+{% endif %}
+{% endif %}
+STEP
+{% if granularity == 'Hour' %}
+toIntervalHour(1)
+{% elsif granularity == 'Day' %}
+toIntervalDay(1)
+{% else %}
+toIntervalMonth(1)
+{% endif %}
diff --git a/etc/clickhouse/queries/key_metrics_periodic__v2.liquid b/etc/clickhouse/queries/key_metrics_periodic__v2.liquid
new file mode 100755
index 0000000..3db2d20
--- /dev/null
+++ b/etc/clickhouse/queries/key_metrics_periodic__v2.liquid
@@ -0,0 +1,56 @@
+SELECT
+ {% if granularity == 'Hour' %}
+ toStartOfHour(timestamp)
+ {% elsif granularity == 'Day' %}
+ toStartOfDay(timestamp)
+ {% else %}
+ toStartOfMonth(timestamp)
+ {% endif %} AS Period,
+ {% if granularity == 'Month' %}
+ uniqExact(user_id) / (date_diff('day', Period, toLastDayOfMonth(Period)) + 1)
+ {% else %}
+ uniqExact(user_id)
+ {% endif %} AS Users,
+ uniqExact(session_id) AS Sessions,
+ count() AS Events
+FROM events
+PREWHERE app_id = '{{app_id}}'
+WHERE 1 = 1
+{% if date_from and date_to %}
+AND timestamp BETWEEN '{{date_from}}' AND '{{date_to}}'
+{% endif %}
+{% if country_code %}
+AND country_code = '{{country_code}}'
+{% endif %}
+{% if event_name %}
+AND event_name = '{{event_name}}'
+{% endif %}
+{% if os_name %}
+AND os_name = '{{os_name}}'
+{% endif %}
+{% if device_model %}
+AND device_model = '{{device_model}}'
+{% endif %}
+{% if app_version %}
+AND app_version = '{{app_version}}'
+{% endif %}
+GROUP by Period
+ORDER BY Period ASC
+WITH FILL
+{% if date_from and date_to %}
+{% if granularity == 'Hour' %}
+FROM toStartOfHour(toDateTime('{{date_from}}')) TO toStartOfHour(toDateTime('{{date_to}}'))
+{% elsif granularity == 'Day' %}
+FROM toStartOfDay(toDateTime('{{date_from}}')) TO toStartOfDay(toDateTime('{{date_to}}'))
+{% else %}
+FROM toStartOfMonth(toDateTime('{{date_from}}')) TO toStartOfMonth(toDateTime('{{date_to}}'))
+{% endif %}
+{% endif %}
+STEP
+{% if granularity == 'Hour' %}
+toIntervalHour(1)
+{% elsif granularity == 'Day' %}
+toIntervalDay(1)
+{% else %}
+toIntervalMonth(1)
+{% endif %}
diff --git a/etc/clickhouse/queries/live_geo__v1.liquid b/etc/clickhouse/queries/live_geo__v1.liquid
new file mode 100755
index 0000000..c89c9e2
--- /dev/null
+++ b/etc/clickhouse/queries/live_geo__v1.liquid
@@ -0,0 +1,14 @@
+SELECT uniq(user_id) as Users,
+country_code as CountryCode,
+region_name as RegionName
+FROM (
+SELECT session_id,
+ min(min_timestamp) AS timestamp,
+ any(user_id) AS user_id,
+ any(country_code) AS country_code,
+ any(region_name) AS region_name
+FROM sessions_live_v1
+WHERE app_id = '{{app_id}}'
+GROUP BY session_id
+HAVING timestamp >= now() - INTERVAL 1 HOUR
+) GROUP BY country_code, region_name
\ No newline at end of file
diff --git a/etc/clickhouse/queries/live_session_details__v1.liquid b/etc/clickhouse/queries/live_session_details__v1.liquid
new file mode 100755
index 0000000..7669ef4
--- /dev/null
+++ b/etc/clickhouse/queries/live_session_details__v1.liquid
@@ -0,0 +1,18 @@
+SELECT session_id as Id,
+ countMerge(events_count) as EventsCount,
+ min(min_timestamp) as StartedAt,
+ max(max_timestamp) - min(min_timestamp) as Duration,
+ countMerge(events_count) as EventsCount,
+ any(app_version) as AppVersion,
+ any(country_code) as CountryCode,
+ any(region_name) as RegionName,
+ any(os_name) as OsName,
+ any(os_version) as OsVersion,
+ groupArrayMerge(events_name) as EventsName,
+ groupArrayMerge(events_timestamp) as EventsTimestamp,
+ groupArrayMerge(events_string_props) as EventsStringProps,
+ groupArrayMerge(events_numeric_props) as EventsNumericProps
+FROM sessions_live_v1
+WHERE app_id = '{{app_id}}'
+AND session_id = '{{session_id}}'
+GROUP BY session_id
\ No newline at end of file
diff --git a/etc/clickhouse/queries/live_sessions__v1.liquid b/etc/clickhouse/queries/live_sessions__v1.liquid
new file mode 100755
index 0000000..201a230
--- /dev/null
+++ b/etc/clickhouse/queries/live_sessions__v1.liquid
@@ -0,0 +1,15 @@
+SELECT session_id as Id,
+ min(min_timestamp) as StartedAt,
+ max(max_timestamp) - min(min_timestamp) as Duration,
+ countMerge(events_count) as EventsCount,
+ any(app_version) as AppVersion,
+ any(country_code) as CountryCode,
+ any(region_name) as RegionName,
+ any(os_name) as OsName,
+ any(os_version) as OsVersion
+FROM sessions_live_v1
+WHERE app_id = '{{app_id}}'
+GROUP BY session_id
+HAVING StartedAt >= now() - INTERVAL 1 HOUR
+ORDER BY Duration DESC
+LIMIT 6
\ No newline at end of file
diff --git a/etc/clickhouse/queries/monthly_usage__v1.liquid b/etc/clickhouse/queries/monthly_usage__v1.liquid
new file mode 100755
index 0000000..19aa24a
--- /dev/null
+++ b/etc/clickhouse/queries/monthly_usage__v1.liquid
@@ -0,0 +1,9 @@
+SELECT toYear(Date) as Year, toMonth(Date) as Month, Events
+FROM (
+ SELECT period as Date,
+ countMerge(events) as Events
+ FROM monthly_usage_v1
+ WHERE app_id = '{{app_id}}'
+ GROUP BY period
+ ORDER BY period WITH FILL TO toStartOfMonth(now() + INTERVAL 1 MONTH) STEP toIntervalMonth(1)
+) ORDER BY Year DESC, Month DESC
\ No newline at end of file
diff --git a/etc/clickhouse/queries/top_n__v1.liquid b/etc/clickhouse/queries/top_n__v1.liquid
new file mode 100755
index 0000000..ed5e3f5
--- /dev/null
+++ b/etc/clickhouse/queries/top_n__v1.liquid
@@ -0,0 +1,26 @@
+SELECT {{name_column}} as Name,
+ {% if value_column == 'UniqueSessions' %}
+ uniqExact(session_id) as Value
+ {% else %}
+ count() as Value
+ {% endif %}
+FROM events
+PREWHERE app_id = '{{app_id}}'
+WHERE 1 = 1
+{% if date_from and date_to %}
+AND timestamp BETWEEN '{{date_from}}' AND '{{date_to}}'
+{% endif %}
+{% if country_code %}
+AND country_code = '{{country_code}}'
+{% endif %}
+{% if event_name %}
+AND event_name = '{{event_name}}'
+{% endif %}
+{% if os_name %}
+AND os_name = '{{os_name}}'
+{% endif %}
+{% if app_version %}
+AND app_version = '{{app_version}}'
+{% endif %}
+GROUP BY Name
+ORDER BY Value DESC
\ No newline at end of file
diff --git a/etc/clickhouse/queries/top_n__v2.liquid b/etc/clickhouse/queries/top_n__v2.liquid
new file mode 100755
index 0000000..111730a
--- /dev/null
+++ b/etc/clickhouse/queries/top_n__v2.liquid
@@ -0,0 +1,29 @@
+SELECT {{name_column}} as Name,
+ {% if value_column == 'UniqueSessions' %}
+ uniqExact(session_id) as Value
+ {% else %}
+ count() as Value
+ {% endif %}
+FROM events
+PREWHERE app_id = '{{app_id}}'
+WHERE 1 = 1
+{% if date_from and date_to %}
+AND timestamp BETWEEN '{{date_from}}' AND '{{date_to}}'
+{% endif %}
+{% if country_code %}
+AND country_code = '{{country_code}}'
+{% endif %}
+{% if event_name %}
+AND event_name = '{{event_name}}'
+{% endif %}
+{% if os_name %}
+AND os_name = '{{os_name}}'
+{% endif %}
+{% if device_model %}
+AND device_model = '{{device_model}}'
+{% endif %}
+{% if app_version %}
+AND app_version = '{{app_version}}'
+{% endif %}
+GROUP BY Name
+ORDER BY Value DESC
\ No newline at end of file
diff --git a/etc/clickhouse/queries/top_props__v1.liquid b/etc/clickhouse/queries/top_props__v1.liquid
new file mode 100755
index 0000000..192b18d
--- /dev/null
+++ b/etc/clickhouse/queries/top_props__v1.liquid
@@ -0,0 +1,34 @@
+SELECT string_arr.1 AS StringKey,
+ string_arr.2 AS StringValue,
+ numeric_arr.1 AS NumericKey,
+ count() AS Events,
+ median(numeric_arr.2) AS Median,
+ min(numeric_arr.2) AS Min,
+ max(numeric_arr.2) AS Max,
+ sum(numeric_arr.2) AS Sum
+FROM (
+ SELECT * FROM (
+ SELECT JSONExtractKeysAndValues(string_props, 'String') AS string_arr,
+ JSONExtractKeysAndValues(numeric_props, 'Float') AS numeric_arr
+ FROM events
+ PREWHERE app_id = '{{app_id}}'
+ WHERE 1 = 1
+ {% if date_from and date_to %}
+ AND timestamp BETWEEN '{{date_from}}' AND '{{date_to}}'
+ {% endif %}
+ {% if country_code %}
+ AND country_code = '{{country_code}}'
+ {% endif %}
+ {% if event_name %}
+ AND event_name = '{{event_name}}'
+ {% endif %}
+ {% if os_name %}
+ AND os_name = '{{os_name}}'
+ {% endif %}
+ {% if app_version %}
+ AND app_version = '{{app_version}}'
+ {% endif %}
+ ) LEFT ARRAY JOIN string_arr
+) LEFT ARRAY JOIN numeric_arr
+GROUP BY StringKey, StringValue, NumericKey
+ORDER BY StringKey, Events DESC
\ No newline at end of file
diff --git a/etc/clickhouse/queries/top_props__v2.liquid b/etc/clickhouse/queries/top_props__v2.liquid
new file mode 100755
index 0000000..4c33eda
--- /dev/null
+++ b/etc/clickhouse/queries/top_props__v2.liquid
@@ -0,0 +1,37 @@
+SELECT string_arr.1 AS StringKey,
+ string_arr.2 AS StringValue,
+ numeric_arr.1 AS NumericKey,
+ count() AS Events,
+ median(numeric_arr.2) AS Median,
+ min(numeric_arr.2) AS Min,
+ max(numeric_arr.2) AS Max,
+ sum(numeric_arr.2) AS Sum
+FROM (
+ SELECT * FROM (
+ SELECT JSONExtractKeysAndValues(string_props, 'String') AS string_arr,
+ JSONExtractKeysAndValues(numeric_props, 'Float') AS numeric_arr
+ FROM events
+ PREWHERE app_id = '{{app_id}}'
+ WHERE 1 = 1
+ {% if date_from and date_to %}
+ AND timestamp BETWEEN '{{date_from}}' AND '{{date_to}}'
+ {% endif %}
+ {% if country_code %}
+ AND country_code = '{{country_code}}'
+ {% endif %}
+ {% if event_name %}
+ AND event_name = '{{event_name}}'
+ {% endif %}
+ {% if os_name %}
+ AND os_name = '{{os_name}}'
+ {% endif %}
+ {% if device_model %}
+ AND device_model = '{{device_model}}'
+ {% endif %}
+ {% if app_version %}
+ AND app_version = '{{app_version}}'
+ {% endif %}
+ ) LEFT ARRAY JOIN string_arr
+) LEFT ARRAY JOIN numeric_arr
+GROUP BY StringKey, StringValue, NumericKey
+ORDER BY StringKey, Events DESC
\ No newline at end of file
diff --git a/etc/geoip/GeoLite2-City.mmdb b/etc/geoip/GeoLite2-City.mmdb
new file mode 100755
index 0000000..9d4c813
Binary files /dev/null and b/etc/geoip/GeoLite2-City.mmdb differ
diff --git a/etc/geoip/coordinates.json b/etc/geoip/coordinates.json
new file mode 100755
index 0000000..ec6300e
--- /dev/null
+++ b/etc/geoip/coordinates.json
@@ -0,0 +1,14886 @@
+{
+ "IR": {
+ "Māzandarān": {
+ "lat": 36.3993691,
+ "lng": 52.1912307
+ },
+ "North Khorasan": {
+ "lat": 37.3744224,
+ "lng": 57.2818625
+ },
+ "Qazvin Province": {
+ "lat": 36.0881317,
+ "lng": 49.8547266
+ },
+ "Khuzestan": {
+ "lat": 31.4360149,
+ "lng": 49.041312
+ },
+ "West Azerbaijan Province": {
+ "lat": 37.7595018,
+ "lng": 45
+ },
+ "Kermanshah Province": {
+ "lat": 34.4576233,
+ "lng": 46.670534
+ },
+ "Gilan Province": {
+ "lat": 37.1171617,
+ "lng": 49.5279996
+ },
+ "East Azerbaijan Province": {
+ "lat": 38.4280669,
+ "lng": 45.9071112
+ },
+ "Hamadan Province": {
+ "lat": 34.9736585,
+ "lng": 48.558749
+ },
+ "Golestan": {
+ "lat": 37.0230398,
+ "lng": 55.5898826
+ },
+ "Markazi": {
+ "lat": 34.6961791,
+ "lng": 49.6911374
+ },
+ "Tehran": {
+ "lat": 35.7218583,
+ "lng": 51.3346954
+ },
+ "Kerman": {
+ "lat": 30.2839379,
+ "lng": 57.0833628
+ },
+ "Kohgiluyeh and Boyer-Ahmad Province": {
+ "lat": 30.724586,
+ "lng": 50.8456323
+ },
+ "Lorestan Province": {
+ "lat": 33.5818394,
+ "lng": 48.3988186
+ },
+ "Zanjan": {
+ "lat": 36.6830045,
+ "lng": 48.5087209
+ },
+ "Yazd Province": {
+ "lat": 32.1006387,
+ "lng": 54.4342138
+ },
+ "Razavi Khorasan": {
+ "lat": 35.1020253,
+ "lng": 59.1041758
+ },
+ "Alborz Province": {
+ "lat": 35.8898715,
+ "lng": 50.8456323
+ },
+ "Fars": {
+ "lat": 29.1043813,
+ "lng": 53.045893
+ },
+ "Hormozgan": {
+ "lat": 27.4150286,
+ "lng": 56.741207
+ },
+ "Chaharmahal and Bakhtiari Province": {
+ "lat": 32.1461389,
+ "lng": 50.5135589
+ },
+ "Bushehr Province": {
+ "lat": 28.7620739,
+ "lng": 51.5150077
+ },
+ "Semnan Province": {
+ "lat": 35.2255585,
+ "lng": 54.4342138
+ },
+ "Kurdistan Province": {
+ "lat": 35.382438,
+ "lng": 47.1362125
+ },
+ "Ardabil Province": {
+ "lat": 38.3180808,
+ "lng": 48.199974
+ },
+ "Qom Province": {
+ "lat": 34.6976594,
+ "lng": 50.8456323
+ },
+ "Isfahan": {
+ "lat": 32.6538966,
+ "lng": 51.66596560000001
+ },
+ "Ilam Province": {
+ "lat": 33.29576180000001,
+ "lng": 46.670534
+ },
+ "South Khorasan Province": {
+ "lat": 32.5175643,
+ "lng": 59.1041758
+ },
+ "Sistan and Baluchestan": {
+ "lat": 27.5299906,
+ "lng": 60.58206759999999
+ },
+ "": {
+ "lat": 32.427908,
+ "lng": 53.688046
+ }
+ },
+ "CY": {
+ "Limassol District": {
+ "lat": 34.6786322,
+ "lng": 33.0413055
+ },
+ "Ammochostos": {
+ "lat": 35.1149116,
+ "lng": 33.919245
+ },
+ "Nicosia": {
+ "lat": 35.1855659,
+ "lng": 33.38227639999999
+ },
+ "Larnaka": {
+ "lat": 34.9182222,
+ "lng": 33.6200625
+ },
+ "Pafos": {
+ "lat": 34.77539489999999,
+ "lng": 32.4217786
+ },
+ "Keryneia": {
+ "lat": 35.299194,
+ "lng": 33.2363246
+ },
+ "": {
+ "lat": 35.126413,
+ "lng": 33.429859
+ }
+ },
+ "SO": {
+ "Bakool": {
+ "lat": 4.3657221,
+ "lng": 44.0960311
+ },
+ "Banaadir": {
+ "lat": 2.1065414,
+ "lng": 45.3933422
+ },
+ "Lower Shabeelle": {
+ "lat": 1.8669821,
+ "lng": 44.5501935
+ },
+ "Woqooyi Galbeed": {
+ "lat": 9.542373999999999,
+ "lng": 44.0960311
+ },
+ "Middle Juba": {
+ "lat": 1.5884675,
+ "lng": 42.2362435
+ },
+ "Nugaal": {
+ "lat": 8.1777704,
+ "lng": 48.8799972
+ },
+ "Gedo": {
+ "lat": 2.5584269,
+ "lng": 41.6011814
+ },
+ "Mudug": {
+ "lat": 6.6307569,
+ "lng": 48.3988186
+ },
+ "Togdheer": {
+ "lat": 9.4460587,
+ "lng": 45.29938620000001
+ },
+ "Bari": {
+ "lat": 10.2161213,
+ "lng": 50.3481835
+ },
+ "Awdal": {
+ "lat": 10.6334285,
+ "lng": 43.329466
+ },
+ "Hiiraan": {
+ "lat": 4.321015,
+ "lng": 45.29938620000001
+ },
+ "Bay": {
+ "lat": 2.4825193,
+ "lng": 43.483738
+ },
+ "Sool": {
+ "lat": 8.722155599999999,
+ "lng": 47.7637565
+ },
+ "": {
+ "lat": 5.152149,
+ "lng": 46.199616
+ }
+ },
+ "YE": {
+ "Abyan Governorate": {
+ "lat": 13.6343413,
+ "lng": 46.0563212
+ },
+ "Sanaa Governorate": {
+ "lat": 15.2254869,
+ "lng": 44.5501935
+ },
+ "Muhafazat Hadramaout": {
+ "lat": 15.552727,
+ "lng": 48.516388
+ },
+ "Amanat Alasimah": {
+ "lat": 15.469397,
+ "lng": 44.2289441
+ },
+ "Al Bayda": {
+ "lat": 13.9889146,
+ "lng": 45.5771002
+ },
+ "Raymah": {
+ "lat": 14.883333,
+ "lng": 43.55
+ },
+ "Laḩij": {
+ "lat": 13.0578415,
+ "lng": 44.8832833
+ },
+ "Ibb Governorate": {
+ "lat": 14.1415717,
+ "lng": 44.2479015
+ },
+ "Ḩajjah": {
+ "lat": 15.7030403,
+ "lng": 43.6039301
+ },
+ "Soqatra": {
+ "lat": 12.4634205,
+ "lng": 53.8237385
+ },
+ "Dhamār": {
+ "lat": 14.5455447,
+ "lng": 44.4087348
+ },
+ "Omran": {
+ "lat": 15.6349352,
+ "lng": 43.89597
+ },
+ "Al Jawf": {
+ "lat": 16.7901819,
+ "lng": 45.29938620000001
+ },
+ "Shabwah": {
+ "lat": 14.7546303,
+ "lng": 46.516262
+ },
+ "Al Hudaydah": {
+ "lat": 14.7909118,
+ "lng": 42.9708838
+ },
+ "Ta‘izz": {
+ "lat": 13.5775886,
+ "lng": 44.0177989
+ },
+ "Al Mahwit Governorate": {
+ "lat": 15.3963229,
+ "lng": 43.5606946
+ },
+ "Al Mahrah Governorate": {
+ "lat": 16.5238423,
+ "lng": 51.6834275
+ },
+ "Aḑ Ḑāli‘": {
+ "lat": 13.7032564,
+ "lng": 44.7334536
+ },
+ "Aden": {
+ "lat": 12.7854969,
+ "lng": 45.0186548
+ },
+ "Ma’rib": {
+ "lat": 15.515888,
+ "lng": 45.4498065
+ },
+ "Şa‘dah": {
+ "lat": 16.9509413,
+ "lng": 43.7477743
+ },
+ "": {
+ "lat": 15.552727,
+ "lng": 48.516388
+ }
+ },
+ "LY": {
+ "Al Butnan": {
+ "lat": 29.7579854,
+ "lng": 23.7632828
+ },
+ "Al Jabal al Akhdar": {
+ "lat": 32.4032332,
+ "lng": 21.6660725
+ },
+ "Darnah": {
+ "lat": 32.755613,
+ "lng": 22.6377432
+ },
+ "Sha'biyat Banghazi": {
+ "lat": 32.1194242,
+ "lng": 20.0867909
+ },
+ "Al Kufrah": {
+ "lat": 23.3112389,
+ "lng": 21.8568586
+ },
+ "Al Marj": {
+ "lat": 32.4981803,
+ "lng": 20.8369932
+ },
+ "Al Wahat": {
+ "lat": 29.0466808,
+ "lng": 21.8568586
+ },
+ "An Nuqat al Khams": {
+ "lat": 32.6914909,
+ "lng": 11.8891721
+ },
+ "Sha'biyat Misratah": {
+ "lat": 31.347767,
+ "lng": 14.4723301
+ },
+ "Al Jufrah": {
+ "lat": 27.9835135,
+ "lng": 16.912251
+ },
+ "Tripoli": {
+ "lat": 32.8872094,
+ "lng": 13.1913383
+ },
+ "Surt": {
+ "lat": 31.189689,
+ "lng": 16.5701927
+ },
+ "Az Zawiyah": {
+ "lat": 32.7630282,
+ "lng": 12.7364962
+ },
+ "Sabha": {
+ "lat": 27.0365406,
+ "lng": 14.4290236
+ },
+ "Nalut": {
+ "lat": 31.8742348,
+ "lng": 10.9750484
+ },
+ "Murzuq": {
+ "lat": 25.9182262,
+ "lng": 13.9260001
+ },
+ "Jabal al Gharbi": {
+ "lat": 30.26380319999999,
+ "lng": 12.8054753
+ },
+ "Ghat": {
+ "lat": 24.9640371,
+ "lng": 10.1759285
+ },
+ "Wadi al Hayat": {
+ "lat": 26.4225926,
+ "lng": 12.6216211
+ },
+ "Al Marqab": {
+ "lat": 32.4599677,
+ "lng": 14.1001326
+ },
+ "": {
+ "lat": 26.3351,
+ "lng": 17.228331
+ }
+ },
+ "IQ": {
+ "Basra": {
+ "lat": 30.5257657,
+ "lng": 47.77379699999999
+ },
+ "Duhok": {
+ "lat": 36.8632107,
+ "lng": 42.9884805
+ },
+ "Salah ad Din": {
+ "lat": 34.5337527,
+ "lng": 43.483738
+ },
+ "Sulaymaniyah": {
+ "lat": 35.5557603,
+ "lng": 45.4351181
+ },
+ "Maysan": {
+ "lat": 31.83790119999999,
+ "lng": 47.1420675
+ },
+ "Kirkuk": {
+ "lat": 35.4666329,
+ "lng": 44.3798895
+ },
+ "Muhafazat Karbala'": {
+ "lat": 32.4045493,
+ "lng": 43.8673222
+ },
+ "Erbil": {
+ "lat": 36.190073,
+ "lng": 43.9930303
+ },
+ "Diyālá": {
+ "lat": 33.7733487,
+ "lng": 45.1494505
+ },
+ "Baghdad": {
+ "lat": 33.315241,
+ "lng": 44.3660671
+ },
+ "Muhafazat Wasit": {
+ "lat": 32.6024094,
+ "lng": 45.7520985
+ },
+ "Al Anbar": {
+ "lat": 32.5597614,
+ "lng": 41.9196471
+ },
+ "Dhi Qar": {
+ "lat": 31.1042292,
+ "lng": 46.3624686
+ },
+ "An Najaf": {
+ "lat": 32.0106646,
+ "lng": 44.3265272
+ },
+ "Nineveh": {
+ "lat": 36.229574,
+ "lng": 42.2362435
+ },
+ "Muhafazat Babil": {
+ "lat": 32.468191,
+ "lng": 44.5501935
+ },
+ "Muhafazat al Qadisiyah": {
+ "lat": 32.043691,
+ "lng": 45.1494505
+ },
+ "": {
+ "lat": 33.223191,
+ "lng": 43.679291
+ }
+ },
+ "SA": {
+ "Medina Region": {
+ "lat": 24.8403977,
+ "lng": 39.3206241
+ },
+ "Al-Qassim Region": {
+ "lat": 26.207826,
+ "lng": 43.483738
+ },
+ "Riyadh Region": {
+ "lat": 22.7554385,
+ "lng": 46.2091547
+ },
+ "Tabuk Region": {
+ "lat": 28.2492372,
+ "lng": 36.6093926
+ },
+ "Eastern Province": {
+ "lat": 23.5681347,
+ "lng": 50.6793759
+ },
+ "'Asir Region": {
+ "lat": 19.0969062,
+ "lng": 42.8637875
+ },
+ "Al Jawf Region": {
+ "lat": 29.887356,
+ "lng": 39.3206241
+ },
+ "Jazan Region": {
+ "lat": 17.1738176,
+ "lng": 42.7076107
+ },
+ "Northern Borders Region": {
+ "lat": 30.0799162,
+ "lng": 42.8637875
+ },
+ "Mecca Region": {
+ "lat": 21.5235584,
+ "lng": 41.9196471
+ },
+ "Najran Region": {
+ "lat": 18.3514664,
+ "lng": 45.6007108
+ },
+ "Ha'il Region": {
+ "lat": 27.7076143,
+ "lng": 41.9196471
+ },
+ "Al Bahah Region": {
+ "lat": 20.2722739,
+ "lng": 41.441251
+ },
+ "": {
+ "lat": 23.885942,
+ "lng": 45.079162
+ }
+ },
+ "AO": {
+ "Lunda Sul": {
+ "lat": -10.2866578,
+ "lng": 20.7122465
+ },
+ "Luanda Norte": {
+ "lat": -8.352502200000002,
+ "lng": 19.1880047
+ },
+ "Moxico": {
+ "lat": -13.4293579,
+ "lng": 20.3308814
+ },
+ "Luanda Province": {
+ "lat": -9.035088,
+ "lng": 13.2663479
+ },
+ "Uíge": {
+ "lat": -7.6102105,
+ "lng": 15.0615865
+ },
+ "Zaire": {
+ "lat": -6.573345799999999,
+ "lng": 13.1740348
+ },
+ "Malanje Province": {
+ "lat": -9.8251183,
+ "lng": 16.912251
+ },
+ "Bengo Province": {
+ "lat": -9.104225699999999,
+ "lng": 13.7289167
+ },
+ "Cuanza Norte Province": {
+ "lat": -9.2398513,
+ "lng": 14.6587821
+ },
+ "Kwanza Sul": {
+ "lat": -10.595191,
+ "lng": 15.4068079
+ },
+ "Huíla": {
+ "lat": -15.0507288,
+ "lng": 13.5451374
+ },
+ "Cunene Province": {
+ "lat": -16.2802221,
+ "lng": 16.1580937
+ },
+ "Namibe Province": {
+ "lat": -15.7979942,
+ "lng": 12.4380581
+ },
+ "Cuando Cobango": {
+ "lat": -16.4180824,
+ "lng": 18.8076195
+ },
+ "Benguela": {
+ "lat": -12.5905158,
+ "lng": 13.416501
+ },
+ "Bíe": {
+ "lat": -12.5727907,
+ "lng": 17.668887
+ },
+ "Huambo": {
+ "lat": -12.7739761,
+ "lng": 15.7468535
+ },
+ "": {
+ "lat": -11.202692,
+ "lng": 17.873887
+ }
+ },
+ "AZ": {
+ "Nakhichevan": {
+ "lat": 39.3256814,
+ "lng": 45.4912647
+ },
+ "Lankaran Rayon": {
+ "lat": 38.6862484,
+ "lng": 48.79128679999999
+ },
+ "Astara": {
+ "lat": 38.4687834,
+ "lng": 48.8728029
+ },
+ "Zaqatala Rayon": {
+ "lat": 41.5906889,
+ "lng": 46.7240373
+ },
+ "Yevlax City": {
+ "lat": 40.6196638,
+ "lng": 47.1500324
+ },
+ "Ujar Rayon": {
+ "lat": 40.4250377,
+ "lng": 47.76990929999999
+ },
+ "Sumqayit City": {
+ "lat": 40.5854765,
+ "lng": 49.6317411
+ },
+ "Shamkir Rayon": {
+ "lat": 40.88123969999999,
+ "lng": 46.0179009
+ },
+ "Qusar Rayon": {
+ "lat": 41.4206965,
+ "lng": 48.1766522
+ },
+ "Qakh Rayon": {
+ "lat": 41.3756182,
+ "lng": 46.801388
+ },
+ "Qabala Rayon": {
+ "lat": 40.9253925,
+ "lng": 47.8016106
+ },
+ "Mingacevir City": {
+ "lat": 40.7702563,
+ "lng": 47.0496015
+ },
+ "Gobustan Rayon": {
+ "lat": 40.5446697,
+ "lng": 48.9550509
+ },
+ "Sabirabad Rayon": {
+ "lat": 39.9384752,
+ "lng": 48.6520675
+ },
+ "Absheron Rayon": {
+ "lat": 40.3629693,
+ "lng": 49.2736815
+ },
+ "Baku City": {
+ "lat": 40.40926169999999,
+ "lng": 49.8670924
+ },
+ "Khachmaz Rayon": {
+ "lat": 41.481364,
+ "lng": 48.7825102
+ },
+ "Ismayilli Rayon": {
+ "lat": 40.7429936,
+ "lng": 48.2125556
+ },
+ "Aghjabadi Rayon": {
+ "lat": 40.0529597,
+ "lng": 47.4553529
+ },
+ "Ganja City": {
+ "lat": 40.6878581,
+ "lng": 46.3723313
+ },
+ "Shamakhi Rayon": {
+ "lat": 40.64272680000001,
+ "lng": 48.6267413
+ },
+ "Barda": {
+ "lat": 40.3706555,
+ "lng": 47.1378909
+ },
+ "": {
+ "lat": 40.143105,
+ "lng": 47.576927
+ }
+ },
+ "TZ": {
+ "Zanzibar Urban/West": {
+ "lat": -6.229813600000001,
+ "lng": 39.2583293
+ },
+ "Pemba North": {
+ "lat": -5.031935199999999,
+ "lng": 39.7755571
+ },
+ "Geita": {
+ "lat": -2.8850378,
+ "lng": 32.2313539
+ },
+ "Arusha": {
+ "lat": -3.3869254,
+ "lng": 36.6829927
+ },
+ "Tanga": {
+ "lat": -5.0888751,
+ "lng": 39.1023228
+ },
+ "Tabora": {
+ "lat": -5.0424945,
+ "lng": 32.8197329
+ },
+ "Rukwa": {
+ "lat": -8.0109444,
+ "lng": 31.4456179
+ },
+ "Simiyu": {
+ "lat": -2.8308738,
+ "lng": 34.1531947
+ },
+ "Shinyanga": {
+ "lat": -3.6809961,
+ "lng": 33.4271403
+ },
+ "Dar es Salaam Region": {
+ "lat": -6.904236500000001,
+ "lng": 39.1959729
+ },
+ "Njombe": {
+ "lat": -9.3731511,
+ "lng": 34.8005067
+ },
+ "Mwanza": {
+ "lat": -2.5164305,
+ "lng": 32.9174517
+ },
+ "Mara": {
+ "lat": -1.7753538,
+ "lng": 34.1531947
+ },
+ "Katavi": {
+ "lat": -6.3677125,
+ "lng": 31.2626366
+ },
+ "Kilimanjaro": {
+ "lat": -3.0674247,
+ "lng": 37.35562729999999
+ },
+ "Morogoro": {
+ "lat": -6.8277556,
+ "lng": 37.6591144
+ },
+ "Mbeya": {
+ "lat": -8.9094014,
+ "lng": 33.4607744
+ },
+ "Iringa": {
+ "lat": -7.768059,
+ "lng": 35.6860723
+ },
+ "Kigoma": {
+ "lat": -4.8824092,
+ "lng": 29.6615055
+ },
+ "Pwani": {
+ "lat": -7.323771400000001,
+ "lng": 38.8205454
+ },
+ "Zanzibar North": {
+ "lat": -5.9395093,
+ "lng": 39.2791011
+ },
+ "Dodoma": {
+ "lat": -6.1811245,
+ "lng": 35.746877
+ },
+ "Pemba South": {
+ "lat": -5.3146961,
+ "lng": 39.7549511
+ },
+ "Kagera": {
+ "lat": -1.3001115,
+ "lng": 31.2626366
+ },
+ "Manyara": {
+ "lat": -4.3150058,
+ "lng": 36.954107
+ },
+ "Ruvuma": {
+ "lat": -10.6878717,
+ "lng": 36.2630846
+ },
+ "Mtwara": {
+ "lat": -10.3112236,
+ "lng": 40.1759806
+ },
+ "Lindi": {
+ "lat": -9.987607599999999,
+ "lng": 39.6982485
+ },
+ "": {
+ "lat": -6.369028,
+ "lng": 34.888822
+ }
+ },
+ "TM": {
+ "Balkan": {
+ "lat": 40.9468884,
+ "lng": 54.4952432
+ },
+ "Ashgabat": {
+ "lat": 37.9600766,
+ "lng": 58.32606289999999
+ },
+ "Ahal": {
+ "lat": 38.6399398,
+ "lng": 59.4720904
+ },
+ "Mary": {
+ "lat": 37.6092461,
+ "lng": 61.86432519999999
+ },
+ "Lebap": {
+ "lat": 38.12724619999999,
+ "lng": 64.7162415
+ },
+ "": {
+ "lat": 38.969719,
+ "lng": 59.556278
+ }
+ },
+ "IL": {
+ "Northern District": {
+ "lat": 32.8972246,
+ "lng": 35.3027226
+ },
+ "Jerusalem": {
+ "lat": 31.768319,
+ "lng": 35.21371
+ },
+ "Central District": {
+ "lat": 31.9521108,
+ "lng": 34.906551
+ },
+ "Southern District": {
+ "lat": 30.829562,
+ "lng": 35.0388164
+ },
+ "Haifa": {
+ "lat": 32.7940463,
+ "lng": 34.989571
+ },
+ "Tel Aviv": {
+ "lat": 32.0852999,
+ "lng": 34.78176759999999
+ },
+ "": {
+ "lat": 31.046051,
+ "lng": 34.851612
+ }
+ },
+ "SY": {
+ "Idlib Governorate": {
+ "lat": 35.8268798,
+ "lng": 36.6957216
+ },
+ "Homs Governorate": {
+ "lat": 34.2567123,
+ "lng": 38.3165725
+ },
+ "Aleppo Governorate": {
+ "lat": 36.2262393,
+ "lng": 37.4681396
+ },
+ "Damascus Governorate": {
+ "lat": 33.5138073,
+ "lng": 36.2765279
+ },
+ "Latakia Governorate": {
+ "lat": 35.6129791,
+ "lng": 36.0023225
+ },
+ "As-Suwayda Governorate": {
+ "lat": 32.7989156,
+ "lng": 36.7819505
+ },
+ "": {
+ "lat": 34.802075,
+ "lng": 38.996815
+ }
+ },
+ "AM": {
+ "Vayots Dzor": {
+ "lat": 39.8172259,
+ "lng": 45.6727334
+ },
+ "Ararat": {
+ "lat": 39.8516615,
+ "lng": 44.6953813
+ },
+ "Syunik": {
+ "lat": 39.2028881,
+ "lng": 46.4798169
+ },
+ "Yerevan": {
+ "lat": 40.1872023,
+ "lng": 44.515209
+ },
+ "Armavir": {
+ "lat": 40.1555525,
+ "lng": 44.0388482
+ },
+ "Kotayk": {
+ "lat": 40.2715286,
+ "lng": 44.63338299999999
+ },
+ "Gegharkunik": {
+ "lat": 40.2500421,
+ "lng": 45.1463314
+ },
+ "Tavush": {
+ "lat": 40.8791442,
+ "lng": 45.1470572
+ },
+ "Lori": {
+ "lat": 40.96984519999999,
+ "lng": 44.4900138
+ },
+ "Shirak": {
+ "lat": 40.8384463,
+ "lng": 43.91423959999999
+ },
+ "Aragatsotn": {
+ "lat": 40.2591212,
+ "lng": 44.1793418
+ },
+ "": {
+ "lat": 40.069099,
+ "lng": 45.038189
+ }
+ },
+ "ZM": {
+ "Luapula Province": {
+ "lat": -11.564831,
+ "lng": 29.0459927
+ },
+ "Western Province": {
+ "lat": -15.9454906,
+ "lng": 23.3823545
+ },
+ "North-Western Province": {
+ "lat": -13.0050258,
+ "lng": 24.9042208
+ },
+ "Northern Province": {
+ "lat": -9.7670177,
+ "lng": 30.8958242
+ },
+ "Copperbelt": {
+ "lat": -13.0570073,
+ "lng": 27.5495846
+ },
+ "Muchinga": {
+ "lat": -11.956818,
+ "lng": 31.2626366
+ },
+ "Southern Province": {
+ "lat": -16.9620634,
+ "lng": 26.419389
+ },
+ "Lusaka Province": {
+ "lat": -15.3657129,
+ "lng": 29.2320784
+ },
+ "Eastern Province": {
+ "lat": -13.8056187,
+ "lng": 31.99280779999999
+ },
+ "Central Province": {
+ "lat": -14.3112263,
+ "lng": 28.299435
+ },
+ "": {
+ "lat": -13.133897,
+ "lng": 27.849332
+ }
+ },
+ "KE": {
+ "Wajir": {
+ "lat": 1.7488388,
+ "lng": 40.058633
+ },
+ "Kakamega": {
+ "lat": 0.2827307,
+ "lng": 34.7518631
+ },
+ "Nairobi": {
+ "lat": -1.2920659,
+ "lng": 36.8219462
+ },
+ "Kiambu": {
+ "lat": -1.1748105,
+ "lng": 36.8304102
+ },
+ "Nyeri": {
+ "lat": -0.437099,
+ "lng": 36.9580104
+ },
+ "Murang'A": {
+ "lat": -0.7236857999999999,
+ "lng": 37.1606968
+ },
+ "Siaya": {
+ "lat": 0.0626293,
+ "lng": 34.2878084
+ },
+ "Kajiado": {
+ "lat": -1.8420731,
+ "lng": 36.7918599
+ },
+ "Narok": {
+ "lat": -1.0875428,
+ "lng": 35.87706420000001
+ },
+ "Laikipia": {
+ "lat": 0.3606063,
+ "lng": 36.7819505
+ },
+ "Nandi": {
+ "lat": 0.1835867,
+ "lng": 35.1268781
+ },
+ "Nakuru": {
+ "lat": -0.3030988,
+ "lng": 36.080026
+ },
+ "Machakos": {
+ "lat": -1.5176837,
+ "lng": 37.2634146
+ },
+ "Kilifi": {
+ "lat": -3.5106508,
+ "lng": 39.9093269
+ },
+ "Mombasa": {
+ "lat": -4.0434771,
+ "lng": 39.6682065
+ },
+ "Marsabit": {
+ "lat": 2.3354966,
+ "lng": 37.9943453
+ },
+ "Migori": {
+ "lat": -1.0706976,
+ "lng": 34.4752715
+ },
+ "Meru": {
+ "lat": 0.0514721,
+ "lng": 37.6456042
+ },
+ "Vihiga": {
+ "lat": 0.03835939999999999,
+ "lng": 34.6967094
+ },
+ "Samburu": {
+ "lat": 1.2154506,
+ "lng": 36.954107
+ },
+ "Mandera District": {
+ "lat": 3.5737991,
+ "lng": 40.958688
+ },
+ "Turkana": {
+ "lat": 3.3122477,
+ "lng": 35.5657862
+ },
+ "Kericho": {
+ "lat": -0.3688967,
+ "lng": 35.286286
+ },
+ "Lamu": {
+ "lat": -2.2695575,
+ "lng": 40.9006408
+ },
+ "Kwale": {
+ "lat": -4.1816115,
+ "lng": 39.4605612
+ },
+ "Kitui": {
+ "lat": -1.3750813,
+ "lng": 37.9952144
+ },
+ "Trans Nzoia": {
+ "lat": 1.0566667,
+ "lng": 34.9506625
+ },
+ "Kisumu": {
+ "lat": -0.0917016,
+ "lng": 34.7679568
+ },
+ "Kisii": {
+ "lat": -0.677334,
+ "lng": 34.779603
+ },
+ "Kirinyaga": {
+ "lat": -0.6590564999999999,
+ "lng": 37.3827234
+ },
+ "Nyamira": {
+ "lat": -0.5669405,
+ "lng": 34.9341234
+ },
+ "Marakwet District": {
+ "lat": 1.0498237,
+ "lng": 35.4781926
+ },
+ "West Pokot District": {
+ "lat": 1.6210076,
+ "lng": 35.3905046
+ },
+ "Baringo": {
+ "lat": 0.8554988,
+ "lng": 36.0893406
+ },
+ "Isiolo": {
+ "lat": 0.355636,
+ "lng": 37.5833061
+ },
+ "Homa Bay": {
+ "lat": -0.5350427,
+ "lng": 34.4530968
+ },
+ "Tana River District": {
+ "lat": -1.6518468,
+ "lng": 39.6518165
+ },
+ "Nyandarua": {
+ "lat": -0.1803855,
+ "lng": 36.5229641
+ },
+ "Garissa": {
+ "lat": -0.4532293,
+ "lng": 39.6460988
+ },
+ "Embu": {
+ "lat": -0.5388381,
+ "lng": 37.4596409
+ },
+ "Uasin Gishu": {
+ "lat": 0.5527638000000001,
+ "lng": 35.3027226
+ },
+ "Tharaka - Nithi": {
+ "lat": -0.2964851,
+ "lng": 37.7237678
+ },
+ "Bungoma": {
+ "lat": 0.5695252,
+ "lng": 34.5583766
+ },
+ "Bomet": {
+ "lat": -0.7777854,
+ "lng": 35.3356518
+ },
+ "": {
+ "lat": -0.023559,
+ "lng": 37.906193
+ }
+ },
+ "RW": {
+ "Northern Province": {
+ "lat": -1.656166,
+ "lng": 29.8815203
+ },
+ "Kigali": {
+ "lat": -1.9440727,
+ "lng": 30.0618851
+ },
+ "Western Province": {
+ "lat": -2.07649,
+ "lng": 29.3250347
+ },
+ "": {
+ "lat": -1.940278,
+ "lng": 29.873888
+ }
+ },
+ "CD": {
+ "Tshopo": {
+ "lat": 0.5455462,
+ "lng": 24.9042208
+ },
+ "Sud-Ubangi": {
+ "lat": 3.2299942,
+ "lng": 19.1880047
+ },
+ "Haut-Lomami": {
+ "lat": -7.705275199999999,
+ "lng": 24.9042208
+ },
+ "Lomami": {
+ "lat": -3.984212297447589,
+ "lng": 24.99751772303499
+ },
+ "Kasaï-Oriental": {
+ "lat": -6.033623,
+ "lng": 23.5728501
+ },
+ "Mongala": {
+ "lat": 1.9962324,
+ "lng": 21.4752851
+ },
+ "Kasai": {
+ "lat": -5.0471979,
+ "lng": 20.7122465
+ },
+ "Sankuru": {
+ "lat": -2.8437453,
+ "lng": 23.3823545
+ },
+ "Maniema": {
+ "lat": -3.0730929,
+ "lng": 26.0413889
+ },
+ "Tanganyika": {
+ "lat": -6.274011799999999,
+ "lng": 27.9249002
+ },
+ "Haut-Uele": {
+ "lat": 3.5845154,
+ "lng": 28.299435
+ },
+ "Nord Kivu": {
+ "lat": -0.7917729,
+ "lng": 29.0459927
+ },
+ "Nord-Ubangi": {
+ "lat": 3.8073246,
+ "lng": 20.7122465
+ },
+ "Ituri": {
+ "lat": 1.8754518,
+ "lng": 29.0459927
+ },
+ "Kasai-Central": {
+ "lat": -6.2514921,
+ "lng": 22.2384017
+ },
+ "South Kivu Province": {
+ "lat": -3.011658,
+ "lng": 28.299435
+ },
+ "Tshuapa": {
+ "lat": -0.5582387999999999,
+ "lng": 21.8568586
+ },
+ "Haut-Katanga": {
+ "lat": -11.0646485,
+ "lng": 27.5495846
+ },
+ "Kwango": {
+ "lat": -6.4337409,
+ "lng": 17.668887
+ },
+ "Kinshasa City": {
+ "lat": -4.3032527,
+ "lng": 15.310528
+ },
+ "Kwilu": {
+ "lat": -4.4863479,
+ "lng": 18.4276047
+ },
+ "Mai-Ndombe": {
+ "lat": -2.6357434,
+ "lng": 18.4276047
+ },
+ "Bas-Congo": {
+ "lat": -5.2365685,
+ "lng": 13.914399
+ },
+ "": {
+ "lat": -4.038333,
+ "lng": 21.758664
+ }
+ },
+ "DJ": {
+ "Tadjourah": {
+ "lat": 11.7873808,
+ "lng": 42.8810929
+ },
+ "Obock": {
+ "lat": 11.9647465,
+ "lng": 43.2884071
+ },
+ "Djibouti": {
+ "lat": 11.825138,
+ "lng": 42.590275
+ },
+ "Dikhil": {
+ "lat": 11.1054336,
+ "lng": 42.3704744
+ },
+ "Ali Sabieh Region": {
+ "lat": 11.1928973,
+ "lng": 42.941698
+ },
+ "Arta Region": {
+ "lat": 11.418236,
+ "lng": 42.4724578
+ },
+ "": {
+ "lat": 11.825138,
+ "lng": 42.590275
+ }
+ },
+ "UG": {
+ "Central Region": {
+ "lat": 0.2540775,
+ "lng": 31.99280779999999
+ },
+ "Western Region": {
+ "lat": 0.2580521,
+ "lng": 30.5279096
+ },
+ "Eastern Region": {
+ "lat": 1.2692186,
+ "lng": 33.438353
+ },
+ "Northern Region": {
+ "lat": 2.8780034,
+ "lng": 32.7181375
+ },
+ "": {
+ "lat": 1.373333,
+ "lng": 32.290275
+ }
+ },
+ "CF": {
+ "Haut-Mbomou": {
+ "lat": 6.2537134,
+ "lng": 25.4733554
+ },
+ "Mbomou": {
+ "lat": 5.556837000000001,
+ "lng": 23.7632828
+ },
+ "Vakaga": {
+ "lat": 9.5113296,
+ "lng": 22.2384017
+ },
+ "Haute-Kotto": {
+ "lat": 7.7964379,
+ "lng": 23.3823545
+ },
+ "Bamingui-Bangoran": {
+ "lat": 8.2733455,
+ "lng": 20.7122465
+ },
+ "Basse-Kotto": {
+ "lat": 4.8719319,
+ "lng": 21.2845025
+ },
+ "Ouaka": {
+ "lat": 6.3168216,
+ "lng": 20.7122465
+ },
+ "Ouham-Pendé": {
+ "lat": 6.6562516,
+ "lng": 15.9699878
+ },
+ "Sangha-Mbaéré": {
+ "lat": 3.4433928,
+ "lng": 15.9699878
+ },
+ "Lobaye": {
+ "lat": 4.038521,
+ "lng": 17.4795173
+ },
+ "Nana-Grébizi": {
+ "lat": 7.184860700000001,
+ "lng": 19.3783206
+ },
+ "Ouham": {
+ "lat": 7.090910999999999,
+ "lng": 17.668887
+ },
+ "Kémo": {
+ "lat": 5.8867794,
+ "lng": 19.3783206
+ },
+ "Ombella-M'Poko": {
+ "lat": 5.1188825,
+ "lng": 18.4276047
+ },
+ "Mambéré-Kadéï": {
+ "lat": 4.541792,
+ "lng": 16.1580937
+ },
+ "Nana-Mambéré": {
+ "lat": 5.8514026,
+ "lng": 15.4068079
+ },
+ "Bangui": {
+ "lat": 4.3946735,
+ "lng": 18.5581899
+ },
+ "": {
+ "lat": 6.611111,
+ "lng": 20.939444
+ }
+ },
+ "SC": {
+ "English River": {
+ "lat": -4.6149553,
+ "lng": 55.4540841
+ },
+ "Takamaka": {
+ "lat": -4.7843688,
+ "lng": 55.5081012
+ },
+ "Port Glaud": {
+ "lat": -4.6488523,
+ "lng": 55.41947529999999
+ },
+ "Plaisance": {
+ "lat": -4.6489918,
+ "lng": 55.4610075
+ },
+ "Glacis": {
+ "lat": -4.5719729,
+ "lng": 55.4416234
+ },
+ "Cascade": {
+ "lat": -4.670554099999999,
+ "lng": 55.49701810000001
+ },
+ "Beau Vallon": {
+ "lat": -4.6210967,
+ "lng": 55.4277802
+ },
+ "Anse Royale": {
+ "lat": -4.740798799999999,
+ "lng": 55.5081012
+ },
+ "Au Cap": {
+ "lat": -4.7059723,
+ "lng": 55.5081012
+ },
+ "Grand Anse Praslin": {
+ "lat": -4.3190725,
+ "lng": 55.69395129999999
+ },
+ "Anse-aux-Pins": {
+ "lat": -4.6900443,
+ "lng": 55.5150289
+ },
+ "Pointe Larue": {
+ "lat": -4.680489,
+ "lng": 55.51918569999999
+ },
+ "Baie Lazare": {
+ "lat": -4.7482525,
+ "lng": 55.4859363
+ },
+ "La Digue": {
+ "lat": -4.3590972,
+ "lng": 55.8412424
+ },
+ "Mont Fleuri": {
+ "lat": -4.6356543,
+ "lng": 55.4554688
+ },
+ "Anse Etoile": {
+ "lat": -4.5962834,
+ "lng": 55.4499303
+ },
+ "Baie Sainte Anne": {
+ "lat": -4.3496065,
+ "lng": 55.76200859999999
+ },
+ "Bel Air": {
+ "lat": -4.6440325,
+ "lng": 55.4499303
+ },
+ "Mont Buxton": {
+ "lat": -4.6166667,
+ "lng": 55.4457768
+ },
+ "Les Mamelles": {
+ "lat": -4.6539526,
+ "lng": 55.4720861
+ },
+ "": {
+ "lat": -4.679574,
+ "lng": 55.491977
+ }
+ },
+ "TD": {
+ "Sila": {
+ "lat": 12.13074,
+ "lng": 21.2845025
+ },
+ "Ennedi-Ouest": {
+ "lat": 19.1593285,
+ "lng": 20.3308814
+ },
+ "Wadi Fira Region": {
+ "lat": 15.0892416,
+ "lng": 21.4752851
+ },
+ "Salamat Region": {
+ "lat": 10.9691601,
+ "lng": 20.7122465
+ },
+ "Ouadaï": {
+ "lat": 13.5226612,
+ "lng": 21.2845025
+ },
+ "Chari-Baguirmi Region": {
+ "lat": 11.2564207,
+ "lng": 16.1580937
+ },
+ "Mayo-Kebbi Ouest": {
+ "lat": 9.4046039,
+ "lng": 14.8454619
+ },
+ "Moyen-Chari Region": {
+ "lat": 9.563214799999999,
+ "lng": 18.6175626
+ },
+ "Barh el Gazel": {
+ "lat": 14.7702266,
+ "lng": 16.912251
+ },
+ "Tandjilé": {
+ "lat": 9.5,
+ "lng": 16.5
+ },
+ "Kanem Region": {
+ "lat": 14.8781262,
+ "lng": 15.4068079
+ },
+ "N’Djaména": {
+ "lat": 12.1348457,
+ "lng": 15.0557415
+ },
+ "Logone Occidental Region": {
+ "lat": 8.6862748,
+ "lng": 15.5943388
+ },
+ "Guéra": {
+ "lat": 11.818682,
+ "lng": 18.4276047
+ },
+ "Mandoul": {
+ "lat": 8.603091,
+ "lng": 17.4795173
+ },
+ "Hadjer-Lamis": {
+ "lat": 12.4577273,
+ "lng": 16.7234639
+ },
+ "Borkou Region": {
+ "lat": 18.0137973,
+ "lng": 17.2902839
+ },
+ "Mayo-Kebbi Est": {
+ "lat": 10.4113014,
+ "lng": 15.5943388
+ },
+ "Lac Region": {
+ "lat": 13.3090183,
+ "lng": 14.4723301
+ },
+ "Logone Oriental Region": {
+ "lat": 8.3149949,
+ "lng": 16.3463791
+ },
+ "Batha Region": {
+ "lat": 13.9371775,
+ "lng": 18.4276047
+ },
+ "Tibesti Region": {
+ "lat": 20.9566791,
+ "lng": 17.2902839
+ },
+ "Ennedi-Est": {
+ "lat": 17.7620703,
+ "lng": 23.0011989
+ },
+ "": {
+ "lat": 15.454166,
+ "lng": 18.732207
+ }
+ },
+ "JO": {
+ "Ma’an": {
+ "lat": 30.1926637,
+ "lng": 35.7249409
+ },
+ "Jerash": {
+ "lat": 32.2746515,
+ "lng": 35.8960765
+ },
+ "Amman Governorate": {
+ "lat": 31.9453633,
+ "lng": 35.9283895
+ },
+ "Madaba": {
+ "lat": 31.7193405,
+ "lng": 35.7932432
+ },
+ "Irbid": {
+ "lat": 32.5568095,
+ "lng": 35.846887
+ },
+ "Ajloun": {
+ "lat": 32.3325599,
+ "lng": 35.751742
+ },
+ "Tafielah": {
+ "lat": 30.8337059,
+ "lng": 35.6160558
+ },
+ "Zarqa": {
+ "lat": 32.0608187,
+ "lng": 36.0941795
+ },
+ "Balqa": {
+ "lat": 32.0366806,
+ "lng": 35.728848
+ },
+ "Karak": {
+ "lat": 31.1853497,
+ "lng": 35.7047733
+ },
+ "Mafraq": {
+ "lat": 32.341673,
+ "lng": 36.2020028
+ },
+ "Aqaba": {
+ "lat": 29.5320522,
+ "lng": 35.0063209
+ },
+ "": {
+ "lat": 30.585164,
+ "lng": 36.238414
+ }
+ },
+ "GR": {
+ "Ionian Islands": {
+ "lat": 37.96948986153249,
+ "lng": 21.38023715882525
+ },
+ "West Greece": {
+ "lat": 38.5115496,
+ "lng": 21.5706786
+ },
+ "Peloponnese": {
+ "lat": 37.349722,
+ "lng": 22.352222
+ },
+ "Thessaly": {
+ "lat": 39.6102887,
+ "lng": 22.047637
+ },
+ "South Aegean": {
+ "lat": 37.0855302,
+ "lng": 25.1489215
+ },
+ "Attica": {
+ "lat": 38.0457568,
+ "lng": 23.8584737
+ },
+ "Epirus": {
+ "lat": 39.5706413,
+ "lng": 20.7642843
+ },
+ "Central Greece": {
+ "lat": 38.6043984,
+ "lng": 22.7152131
+ },
+ "Crete": {
+ "lat": 35.240117,
+ "lng": 24.8092691
+ },
+ "Central Macedonia": {
+ "lat": 40.621173,
+ "lng": 23.1918021
+ },
+ "North Aegean": {
+ "lat": 39.074208,
+ "lng": 21.824312
+ },
+ "West Macedonia": {
+ "lat": 40.3004058,
+ "lng": 21.7903559
+ },
+ "East Macedonia and Thrace": {
+ "lat": 41.1295126,
+ "lng": 24.8877191
+ },
+ "Mount Athos": {
+ "lat": 40.2644928,
+ "lng": 24.2152731
+ },
+ "": {
+ "lat": 39.074208,
+ "lng": 21.824312
+ }
+ },
+ "LB": {
+ "Mohafazat Beqaa": {
+ "lat": 33.8462662,
+ "lng": 35.9019489
+ },
+ "Mohafazat Liban-Nord": {
+ "lat": 34.4380625,
+ "lng": 35.8308233
+ },
+ "Mohafazat Mont-Liban": {
+ "lat": 33.8100858,
+ "lng": 35.5973139
+ },
+ "South Governorate": {
+ "lat": 33.2721479,
+ "lng": 35.2032778
+ },
+ "Mohafazat Nabatiye": {
+ "lat": 33.3771693,
+ "lng": 35.4838293
+ },
+ "Mohafazat Aakkar": {
+ "lat": 34.5328763,
+ "lng": 36.1328132
+ },
+ "Beyrouth": {
+ "lat": 33.8937913,
+ "lng": 35.5017767
+ },
+ "Mohafazat Baalbek-Hermel": {
+ "lat": 34.2658556,
+ "lng": 36.3498097
+ },
+ "": {
+ "lat": 33.854721,
+ "lng": 35.862285
+ }
+ },
+ "PS": {
+ "Rafah Governorate": {
+ "lat": 31.29677999999999,
+ "lng": 34.243482
+ },
+ "Deir al-Balah Governorate": {
+ "lat": 31.4216346,
+ "lng": 34.3865392
+ },
+ "Khan Yunis Governorate": {
+ "lat": 31.3546763,
+ "lng": 34.3088255
+ },
+ "North Gaza Governorate": {
+ "lat": 31.5417408,
+ "lng": 34.519604
+ },
+ "Gaza Governorate": {
+ "lat": 31.3546763,
+ "lng": 34.3088255
+ },
+ "Tulkarm Governorate": {
+ "lat": 32.3075097,
+ "lng": 35.1048713
+ },
+ "Tubas Governorate": {
+ "lat": 31.952162,
+ "lng": 35.233154
+ },
+ "Salfit Governorate": {
+ "lat": 32.1087714,
+ "lng": 35.1048713
+ },
+ "Ramallah and al-Bireh Governorate": {
+ "lat": 31.9485955,
+ "lng": 35.1708741
+ },
+ "Qalqilya Governorate": {
+ "lat": 32.1764026,
+ "lng": 35.0167866
+ },
+ "Nablus Governorate": {
+ "lat": 32.2226678,
+ "lng": 35.2621461
+ },
+ "Jenin Governorate": {
+ "lat": 32.397317,
+ "lng": 35.2587964
+ },
+ "Hebron": {
+ "lat": 31.532569,
+ "lng": 35.09982600000001
+ },
+ "Bethlehem Governorate": {
+ "lat": 31.952162,
+ "lng": 35.233154
+ },
+ "Jericho Governorate": {
+ "lat": 31.8611058,
+ "lng": 35.4617583
+ },
+ "Quds Governorate": {
+ "lat": 31.952162,
+ "lng": 35.233154
+ },
+ "": {
+ "lat": 31.952162,
+ "lng": 35.233154
+ }
+ },
+ "KW": {
+ "Muhafazat al Jahra'": {
+ "lat": 29.9931831,
+ "lng": 47.76347310000001
+ },
+ "Al Asimah": {
+ "lat": 29.3342457,
+ "lng": 47.9812152
+ },
+ "Hawalli": {
+ "lat": 29.33778819999999,
+ "lng": 48.0234708
+ },
+ "Al Aḩmadī": {
+ "lat": 29.083461,
+ "lng": 48.07347619999999
+ },
+ "Mubārak al Kabīr": {
+ "lat": 29.1886698,
+ "lng": 48.0806909
+ },
+ "Al Farwaniyah": {
+ "lat": 29.281596,
+ "lng": 47.96025179999999
+ },
+ "": {
+ "lat": 29.31166,
+ "lng": 47.481766
+ }
+ },
+ "OM": {
+ "Muscat": {
+ "lat": 23.5880307,
+ "lng": 58.3828717
+ },
+ "Southeastern Governorate": {
+ "lat": 22.0158249,
+ "lng": 59.3251922
+ },
+ "Al Batinah North Governorate": {
+ "lat": 24.3419846,
+ "lng": 56.7298904
+ },
+ "Ad Dakhiliyah": {
+ "lat": 22.8588758,
+ "lng": 57.5394356
+ },
+ "Al Wusta Governorate": {
+ "lat": 19.9571078,
+ "lng": 56.2756846
+ },
+ "Northeastern Governorate": {
+ "lat": 22.7141196,
+ "lng": 58.5308064
+ },
+ "Dhofar": {
+ "lat": 17.0322121,
+ "lng": 54.1425214
+ },
+ "Musandam Governorate": {
+ "lat": 26.1986144,
+ "lng": 56.2460949
+ },
+ "Ad Dhahirah": {
+ "lat": 23.2161674,
+ "lng": 56.4907444
+ },
+ "Al Batinah South": {
+ "lat": 23.4314903,
+ "lng": 57.4239796
+ },
+ "Al Buraimi": {
+ "lat": 24.2815207,
+ "lng": 55.8245558
+ },
+ "": {
+ "lat": 21.512583,
+ "lng": 55.923255
+ }
+ },
+ "QA": {
+ "Baladiyat Umm Salal": {
+ "lat": 25.4600894,
+ "lng": 51.3260472
+ },
+ "Al Wakrah": {
+ "lat": 25.1659314,
+ "lng": 51.5975524
+ },
+ "Al-Shahaniya": {
+ "lat": 25.4106386,
+ "lng": 51.1846025
+ },
+ "Baladiyat ash Shamal": {
+ "lat": 26.1182743,
+ "lng": 51.2157265
+ },
+ "Baladiyat ar Rayyan": {
+ "lat": 24.9169514,
+ "lng": 51.0540674
+ },
+ "Al Khor": {
+ "lat": 25.6804078,
+ "lng": 51.4968502
+ },
+ "Baladiyat ad Dawhah": {
+ "lat": 25.2854473,
+ "lng": 51.53103979999999
+ },
+ "": {
+ "lat": 25.354826,
+ "lng": 51.183884
+ }
+ },
+ "BH": {
+ "Manama": {
+ "lat": 26.2235305,
+ "lng": 50.5875935
+ },
+ "Northern": {
+ "lat": 26.1882301,
+ "lng": 50.4928628
+ },
+ "Muharraq": {
+ "lat": 26.2534919,
+ "lng": 50.60825579999999
+ },
+ "Southern Governorate": {
+ "lat": 26.0303854,
+ "lng": 50.5549719
+ },
+ "": {
+ "lat": 25.930414,
+ "lng": 50.637772
+ }
+ },
+ "AE": {
+ "Imarat Umm al Qaywayn": {
+ "lat": 25.5256168,
+ "lng": 55.5403105
+ },
+ "Imarat Ra's al Khaymah": {
+ "lat": 25.6741343,
+ "lng": 55.9804173
+ },
+ "Sharjah": {
+ "lat": 25.3561698,
+ "lng": 55.427211
+ },
+ "Dubai": {
+ "lat": 25.2048493,
+ "lng": 55.2707828
+ },
+ "Abu Dhabi": {
+ "lat": 24.453884,
+ "lng": 54.3773438
+ },
+ "Fujairah": {
+ "lat": 25.1288099,
+ "lng": 56.3264849
+ },
+ "Ajman": {
+ "lat": 25.4052165,
+ "lng": 55.5136433
+ },
+ "": {
+ "lat": 23.424076,
+ "lng": 53.847818
+ }
+ },
+ "TR": {
+ "Siirt": {
+ "lat": 37.927404,
+ "lng": 41.94197800000001
+ },
+ "Adana": {
+ "lat": 36.9914194,
+ "lng": 35.3308285
+ },
+ "İzmir Province": {
+ "lat": 38.3591693,
+ "lng": 27.2676116
+ },
+ "Hakkâri": {
+ "lat": 37.577427,
+ "lng": 43.736782
+ },
+ "Antalya": {
+ "lat": 36.8968908,
+ "lng": 30.7133233
+ },
+ "Yozgat": {
+ "lat": 39.821049,
+ "lng": 34.808573
+ },
+ "Şanlıurfa": {
+ "lat": 37.1674039,
+ "lng": 38.7955149
+ },
+ "Hatay": {
+ "lat": 36.202585,
+ "lng": 36.1604041
+ },
+ "Malatya": {
+ "lat": 38.3553627,
+ "lng": 38.33352470000001
+ },
+ "Ankara": {
+ "lat": 39.9333635,
+ "lng": 32.8597419
+ },
+ "Aksaray": {
+ "lat": 38.368626,
+ "lng": 34.0297
+ },
+ "Aydın": {
+ "lat": 37.8380162,
+ "lng": 27.8455601
+ },
+ "Konya": {
+ "lat": 37.8746429,
+ "lng": 32.4931554
+ },
+ "Sivas": {
+ "lat": 39.750545,
+ "lng": 37.0150217
+ },
+ "Diyarbakır Province": {
+ "lat": 38.1066372,
+ "lng": 40.5426896
+ },
+ "Van": {
+ "lat": 38.5012085,
+ "lng": 43.3729793
+ },
+ "Uşak": {
+ "lat": 38.6742286,
+ "lng": 29.4058825
+ },
+ "Niğde Province": {
+ "lat": 38.0993086,
+ "lng": 34.6856509
+ },
+ "Şırnak": {
+ "lat": 37.518974,
+ "lng": 42.453714
+ },
+ "Muğla": {
+ "lat": 37.215374,
+ "lng": 28.363394
+ },
+ "Ağrı": {
+ "lat": 39.719074,
+ "lng": 43.050591
+ },
+ "Manisa": {
+ "lat": 38.6140337,
+ "lng": 27.4295624
+ },
+ "Tunceli": {
+ "lat": 39.10617,
+ "lng": 39.548259
+ },
+ "Bingöl": {
+ "lat": 38.885464,
+ "lng": 40.496625
+ },
+ "Bitlis": {
+ "lat": 38.400569,
+ "lng": 42.109502
+ },
+ "Mersin": {
+ "lat": 36.8121041,
+ "lng": 34.6414811
+ },
+ "Erzincan": {
+ "lat": 39.746797,
+ "lng": 39.491124
+ },
+ "Kayseri": {
+ "lat": 38.720489,
+ "lng": 35.48259700000001
+ },
+ "Balıkesir": {
+ "lat": 39.65329759999999,
+ "lng": 27.8903423
+ },
+ "Elazığ": {
+ "lat": 38.674816,
+ "lng": 39.22251500000001
+ },
+ "Afyonkarahisar Province": {
+ "lat": 38.7391099,
+ "lng": 30.7120023
+ },
+ "Kütahya": {
+ "lat": 39.4199547,
+ "lng": 29.985732
+ },
+ "Eskişehir": {
+ "lat": 39.7667061,
+ "lng": 30.52563109999999
+ },
+ "Mardin": {
+ "lat": 37.312903,
+ "lng": 40.733951
+ },
+ "Kahramanmaraş": {
+ "lat": 37.5752755,
+ "lng": 36.9228223
+ },
+ "Denizli": {
+ "lat": 37.7830159,
+ "lng": 29.0963328
+ },
+ "Batman": {
+ "lat": 37.8895167,
+ "lng": 41.1292832
+ },
+ "Adıyaman Province": {
+ "lat": 37.9078291,
+ "lng": 38.48499229999999
+ },
+ "Isparta": {
+ "lat": 37.7626487,
+ "lng": 30.55370499999999
+ },
+ "Osmaniye": {
+ "lat": 37.0746279,
+ "lng": 36.2464002
+ },
+ "Kilis": {
+ "lat": 36.716477,
+ "lng": 37.114661
+ },
+ "Gaziantep": {
+ "lat": 37.065953,
+ "lng": 37.37811
+ },
+ "Nevşehir Province": {
+ "lat": 38.6939399,
+ "lng": 34.6856509
+ },
+ "Muş": {
+ "lat": 38.734561,
+ "lng": 41.491038
+ },
+ "Kırşehir": {
+ "lat": 39.146078,
+ "lng": 34.1594989
+ },
+ "Kırıkkale": {
+ "lat": 39.8397835,
+ "lng": 33.5088782
+ },
+ "Burdur": {
+ "lat": 37.718336,
+ "lng": 30.282333
+ },
+ "Karaman": {
+ "lat": 37.181009,
+ "lng": 33.22224300000001
+ },
+ "Iğdır": {
+ "lat": 39.92005959999999,
+ "lng": 44.0436151
+ },
+ "Erzurum": {
+ "lat": 39.9054993,
+ "lng": 41.2658236
+ },
+ "Bilecik": {
+ "lat": 40.142573,
+ "lng": 29.97933
+ },
+ "Zonguldak": {
+ "lat": 41.45352099999999,
+ "lng": 31.78938
+ },
+ "Kocaeli": {
+ "lat": 40.7654408,
+ "lng": 29.9408089
+ },
+ "Istanbul": {
+ "lat": 41.0082376,
+ "lng": 28.9783589
+ },
+ "Artvin": {
+ "lat": 41.180937,
+ "lng": 41.820819
+ },
+ "Trabzon": {
+ "lat": 41.0026969,
+ "lng": 39.7167633
+ },
+ "Giresun": {
+ "lat": 40.91753200000001,
+ "lng": 38.392653
+ },
+ "Bursa Province": {
+ "lat": 40.0655459,
+ "lng": 29.2320784
+ },
+ "Karabük Province": {
+ "lat": 41.187489,
+ "lng": 32.7417419
+ },
+ "Gümüşhane Province": {
+ "lat": 40.28036729999999,
+ "lng": 39.3143253
+ },
+ "Yalova": {
+ "lat": 40.654895,
+ "lng": 29.284186
+ },
+ "Kırklareli": {
+ "lat": 41.735472,
+ "lng": 27.224369
+ },
+ "Edirne": {
+ "lat": 41.67712969999999,
+ "lng": 26.5557145
+ },
+ "Ordu": {
+ "lat": 40.986166,
+ "lng": 37.879721
+ },
+ "Bartın": {
+ "lat": 41.637602,
+ "lng": 32.333811
+ },
+ "Sinop": {
+ "lat": 42.02797400000001,
+ "lng": 35.151725
+ },
+ "Kastamonu": {
+ "lat": 41.376625,
+ "lng": 33.776497
+ },
+ "Kars Province": {
+ "lat": 40.2807636,
+ "lng": 42.9919527
+ },
+ "Tokat": {
+ "lat": 40.3234643,
+ "lng": 36.5521928
+ },
+ "Samsun": {
+ "lat": 41.2797031,
+ "lng": 36.3360667
+ },
+ "Tekirdağ": {
+ "lat": 40.9780919,
+ "lng": 27.511674
+ },
+ "Çorum": {
+ "lat": 40.54992560000001,
+ "lng": 34.9537242
+ },
+ "Amasya": {
+ "lat": 40.656455,
+ "lng": 35.837347
+ },
+ "Sakarya": {
+ "lat": 40.7730743,
+ "lng": 30.3948169
+ },
+ "Bolu": {
+ "lat": 40.732541,
+ "lng": 31.608209
+ },
+ "Rize Province": {
+ "lat": 40.95814970000001,
+ "lng": 40.9226985
+ },
+ "Çankırı": {
+ "lat": 40.600207,
+ "lng": 33.616223
+ },
+ "Canakkale": {
+ "lat": 40.14671999999999,
+ "lng": 26.408587
+ },
+ "Duezce": {
+ "lat": 40.83872,
+ "lng": 31.162609
+ },
+ "Bayburt Province": {
+ "lat": 40.3317555,
+ "lng": 40.1437863
+ },
+ "Ardahan": {
+ "lat": 41.11295,
+ "lng": 42.70227999999999
+ },
+ "": {
+ "lat": 38.963745,
+ "lng": 35.243322
+ }
+ },
+ "ET": {
+ "Somali": {
+ "lat": 6.6612293,
+ "lng": 43.7908453
+ },
+ "Oromiya": {
+ "lat": 7.546037699999999,
+ "lng": 40.6346851
+ },
+ "Amhara": {
+ "lat": 11.3494247,
+ "lng": 37.9784585
+ },
+ "Harari Region": {
+ "lat": 9.314866,
+ "lng": 42.1967716
+ },
+ "Gambela": {
+ "lat": 8.2505817,
+ "lng": 34.5877629
+ },
+ "Afar Region": {
+ "lat": 12.1056239,
+ "lng": 40.6346851
+ },
+ "Dire Dawa": {
+ "lat": 9.6048717,
+ "lng": 41.85850809999999
+ },
+ "Southern Nations, Nationalities, and People's Region": {
+ "lat": 6.5156911,
+ "lng": 36.954107
+ },
+ "Bīnshangul Gumuz": {
+ "lat": 10.7802889,
+ "lng": 35.5657862
+ },
+ "Tigray": {
+ "lat": 14.0323336,
+ "lng": 38.3165725
+ },
+ "Addis Ababa": {
+ "lat": 9.0191936,
+ "lng": 38.7524635
+ },
+ "": {
+ "lat": 9.145,
+ "lng": 40.489673
+ }
+ },
+ "ER": {
+ "Northern Red Sea": {
+ "lat": 16.2583997,
+ "lng": 38.8205454
+ },
+ "Anseba Region": {
+ "lat": 16.4745531,
+ "lng": 37.8087693
+ },
+ "Southern Red Sea Region": {
+ "lat": 13.875153,
+ "lng": 41.441251
+ },
+ "Gash-Barka Region": {
+ "lat": 15.4068825,
+ "lng": 37.6386622
+ },
+ "Maekel Region": {
+ "lat": 15.3852689,
+ "lng": 38.904164
+ },
+ "Debub Region": {
+ "lat": 14.9478692,
+ "lng": 39.1543677
+ },
+ "": {
+ "lat": 15.179384,
+ "lng": 39.782334
+ }
+ },
+ "EG": {
+ "Gharbia": {
+ "lat": 30.8753556,
+ "lng": 31.03351
+ },
+ "New Valley": {
+ "lat": 24.5455638,
+ "lng": 27.1735316
+ },
+ "Qalyubia": {
+ "lat": 30.3292368,
+ "lng": 31.2168466
+ },
+ "Dakahlia": {
+ "lat": 31.1656044,
+ "lng": 31.4913182
+ },
+ "Monufia": {
+ "lat": 30.5972455,
+ "lng": 30.9876321
+ },
+ "Beni Suweif": {
+ "lat": 28.8938837,
+ "lng": 31.4456179
+ },
+ "Sohag": {
+ "lat": 26.5590737,
+ "lng": 31.6956705
+ },
+ "Matruh": {
+ "lat": 31.3543445,
+ "lng": 27.2373159
+ },
+ "Cairo Governorate": {
+ "lat": 29.9537564,
+ "lng": 31.5370003
+ },
+ "South Sinai": {
+ "lat": 29.3101828,
+ "lng": 34.1531947
+ },
+ "Beheira": {
+ "lat": 30.8480986,
+ "lng": 30.3435506
+ },
+ "North Sinai": {
+ "lat": 30.282365,
+ "lng": 33.617577
+ },
+ "Qena": {
+ "lat": 26.155061,
+ "lng": 32.7160121
+ },
+ "Kafr el-Sheikh": {
+ "lat": 31.11065929999999,
+ "lng": 30.9387799
+ },
+ "Sharqia": {
+ "lat": 30.7326622,
+ "lng": 31.7195459
+ },
+ "Minya": {
+ "lat": 28.0870967,
+ "lng": 30.7618397
+ },
+ "Asyut": {
+ "lat": 27.1783117,
+ "lng": 31.1859257
+ },
+ "Giza": {
+ "lat": 30.0130557,
+ "lng": 31.2088526
+ },
+ "Faiyum": {
+ "lat": 29.3084021,
+ "lng": 30.8428497
+ },
+ "Damietta Governorate": {
+ "lat": 31.3625799,
+ "lng": 31.6739371
+ },
+ "Aswan": {
+ "lat": 24.088938,
+ "lng": 32.8998293
+ },
+ "Port Said": {
+ "lat": 31.2652893,
+ "lng": 32.3018661
+ },
+ "Suez": {
+ "lat": 29.9668343,
+ "lng": 32.5498069
+ },
+ "Alexandria": {
+ "lat": 31.2000924,
+ "lng": 29.9187387
+ },
+ "Luxor": {
+ "lat": 25.6872431,
+ "lng": 32.6396357
+ },
+ "Ismailia Governorate": {
+ "lat": 30.5830934,
+ "lng": 32.2653887
+ },
+ "Red Sea": {
+ "lat": 24.6826316,
+ "lng": 34.1531947
+ },
+ "": {
+ "lat": 26.820553,
+ "lng": 30.802498
+ }
+ },
+ "AL": {
+ "Vlorë County": {
+ "lat": 40.150096,
+ "lng": 19.8067916
+ },
+ "Gjirokastër County": {
+ "lat": 40.1910995,
+ "lng": 20.2355647
+ },
+ "Korçë County": {
+ "lat": 40.590567,
+ "lng": 20.6168921
+ },
+ "Dibër County": {
+ "lat": 41.5888163,
+ "lng": 20.2355647
+ },
+ "Elbasan County": {
+ "lat": 41.1266672,
+ "lng": 20.2355647
+ },
+ "Kukës County": {
+ "lat": 42.0120271,
+ "lng": 20.4262088
+ },
+ "Tirana": {
+ "lat": 41.3275459,
+ "lng": 19.8186982
+ },
+ "Berat County": {
+ "lat": 40.6953012,
+ "lng": 20.0449662
+ },
+ "Shkodër County": {
+ "lat": 42.150371,
+ "lng": 19.6639309
+ },
+ "Lezhë County": {
+ "lat": 41.7813759,
+ "lng": 19.8067916
+ },
+ "Fier County": {
+ "lat": 40.727504,
+ "lng": 19.5627596
+ },
+ "Durrës County": {
+ "lat": 41.50809719999999,
+ "lng": 19.6163185
+ },
+ "": {
+ "lat": 41.153332,
+ "lng": 20.168331
+ }
+ },
+ "SD": {
+ "Northern": {
+ "lat": 18.4448963,
+ "lng": 30.1589303
+ },
+ "Al Jazīrah": {
+ "lat": 14.4275191,
+ "lng": 33.258793
+ },
+ "Northern Darfur": {
+ "lat": 15.7661969,
+ "lng": 24.9042208
+ },
+ "Khartoum": {
+ "lat": 15.5006544,
+ "lng": 32.5598994
+ },
+ "Sinnār": {
+ "lat": 13.567469,
+ "lng": 33.5672045
+ },
+ "White Nile": {
+ "lat": 13.2403881,
+ "lng": 32.5372741
+ },
+ "Southern Darfur": {
+ "lat": 11.3464512,
+ "lng": 24.524264
+ },
+ "Kassala": {
+ "lat": 15.4581332,
+ "lng": 36.4039629
+ },
+ "Southern Kordofan": {
+ "lat": 11.0365439,
+ "lng": 30.8958242
+ },
+ "Red Sea": {
+ "lat": 19.4556063,
+ "lng": 35.2148469
+ },
+ "Central Darfur": {
+ "lat": 12.3135376,
+ "lng": 23.1918021
+ },
+ "Eastern Darfur": {
+ "lat": 11.1376634,
+ "lng": 26.7967849
+ },
+ "Blue Nile": {
+ "lat": 11.5860078,
+ "lng": 34.1531947
+ },
+ "North Kordofan": {
+ "lat": 15.170596,
+ "lng": 29.4179324
+ },
+ "West Kordofan State": {
+ "lat": 11.6452819,
+ "lng": 28.299435
+ },
+ "Al Qaḑārif": {
+ "lat": 14.024307,
+ "lng": 35.3685679
+ },
+ "Western Darfur": {
+ "lat": 12.8463561,
+ "lng": 23.0011989
+ },
+ "River Nile": {
+ "lat": 17.1142529,
+ "lng": 33.7964613
+ },
+ "": {
+ "lat": 12.862807,
+ "lng": 30.217636
+ }
+ },
+ "SS": {
+ "Central Equatoria": {
+ "lat": 4.6144063,
+ "lng": 31.2626366
+ },
+ "": {
+ "lat": 7.0770786,
+ "lng": 30.2045784
+ }
+ },
+ "BI": {
+ "Makamba Province": {
+ "lat": -4.3257062,
+ "lng": 29.6962677
+ },
+ "Bururi Province": {
+ "lat": -3.8594819,
+ "lng": 29.6499162
+ },
+ "Rumonge": {
+ "lat": -3.9754049,
+ "lng": 29.4388014
+ },
+ "Mwaro": {
+ "lat": -3.4268749,
+ "lng": 29.6393546
+ },
+ "Bujumbura Rural Province": {
+ "lat": -3.512215,
+ "lng": 29.3714909
+ },
+ "Bujumbura Mairie Province": {
+ "lat": -3.3884141,
+ "lng": 29.3482646
+ },
+ "Muramvya Province": {
+ "lat": -3.2898398,
+ "lng": 29.6499162
+ },
+ "Gitega Province": {
+ "lat": -3.4929051,
+ "lng": 29.9277947
+ },
+ "Ruyigi Province": {
+ "lat": -3.446207,
+ "lng": 30.2512728
+ },
+ "Cankuzo Province": {
+ "lat": -3.1527788,
+ "lng": 30.6199895
+ },
+ "Bubanza Province": {
+ "lat": -3.1572403,
+ "lng": 29.3714909
+ },
+ "Cibitoke Province": {
+ "lat": -2.8102897,
+ "lng": 29.1855785
+ },
+ "Ngozi Province": {
+ "lat": -2.8958243,
+ "lng": 29.8815203
+ },
+ "Kayanza Province": {
+ "lat": -3.0077981,
+ "lng": 29.6499162
+ },
+ "Muyinga Province": {
+ "lat": -2.7793511,
+ "lng": 30.29741989999999
+ },
+ "Kirundo Province": {
+ "lat": -2.5762882,
+ "lng": 30.112735
+ },
+ "Rutana Province": {
+ "lat": -3.879152299999999,
+ "lng": 30.0665236
+ },
+ "": {
+ "lat": -3.373056,
+ "lng": 29.918886
+ }
+ },
+ "RU": {
+ "Tver Oblast": {
+ "lat": 57.0021654,
+ "lng": 33.9853142
+ },
+ "Orenburg Oblast": {
+ "lat": 51.76340260000001,
+ "lng": 54.6188188
+ },
+ "Vladimir Oblast": {
+ "lat": 56.1553465,
+ "lng": 40.5926685
+ },
+ "Moscow Oblast": {
+ "lat": 55.340396,
+ "lng": 38.2917651
+ },
+ "Chelyabinsk Oblast": {
+ "lat": 54.43194219999999,
+ "lng": 60.87889629999999
+ },
+ "Rostov Oblast": {
+ "lat": 47.6853247,
+ "lng": 41.8258952
+ },
+ "Volgograd Oblast": {
+ "lat": 49.7604522,
+ "lng": 45
+ },
+ "Kaluga Oblast": {
+ "lat": 54.3872666,
+ "lng": 35.1889094
+ },
+ "Samara Oblast": {
+ "lat": 53.41838389999999,
+ "lng": 50.4725528
+ },
+ "Kursk Oblast": {
+ "lat": 51.76340260000001,
+ "lng": 35.3811812
+ },
+ "Stavropol Kray": {
+ "lat": 44.6680993,
+ "lng": 43.520214
+ },
+ "Kaliningrad Oblast": {
+ "lat": 54.8235292,
+ "lng": 21.4816163
+ },
+ "Moscow": {
+ "lat": 55.755826,
+ "lng": 37.6173
+ },
+ "St.-Petersburg": {
+ "lat": 59.9310584,
+ "lng": 30.3609097
+ },
+ "Tatarstan Republic": {
+ "lat": 55.1802364,
+ "lng": 50.7263945
+ },
+ "Tambov Oblast": {
+ "lat": 52.6416589,
+ "lng": 41.4216451
+ },
+ "Nizhny Novgorod Oblast": {
+ "lat": 55.7995159,
+ "lng": 44.0296769
+ },
+ "Krasnodar Krai": {
+ "lat": 45.6415289,
+ "lng": 39.7055977
+ },
+ "Lipetsk Oblast": {
+ "lat": 52.5264702,
+ "lng": 39.2032269
+ },
+ "Karelia": {
+ "lat": 63.15587019999999,
+ "lng": 32.9905552
+ },
+ "Komi": {
+ "lat": 63.8630539,
+ "lng": 54.8312689
+ },
+ "Perm Krai": {
+ "lat": 58.8231929,
+ "lng": 56.5872481
+ },
+ "Mariy-El Republic": {
+ "lat": 56.438457,
+ "lng": 47.9607757
+ },
+ "Tula Oblast": {
+ "lat": 54.163768,
+ "lng": 37.5649507
+ },
+ "Smolensk Oblast": {
+ "lat": 54.9882994,
+ "lng": 32.6677378
+ },
+ "Kirov Oblast": {
+ "lat": 58.4198529,
+ "lng": 50.2097248
+ },
+ "Yaroslavl Oblast": {
+ "lat": 57.8991523,
+ "lng": 38.8388633
+ },
+ "Chuvashia": {
+ "lat": 55.55959920000001,
+ "lng": 46.9283536
+ },
+ "Bashkortostan Republic": {
+ "lat": 54.2312172,
+ "lng": 56.1645257
+ },
+ "Leningrad Oblast": {
+ "lat": 60.0793208,
+ "lng": 31.8926644
+ },
+ "Adygeya Republic": {
+ "lat": 44.8229155,
+ "lng": 40.1754463
+ },
+ "Vologda Oblast": {
+ "lat": 59.8706711,
+ "lng": 40.6555411
+ },
+ "Udmurtiya Republic": {
+ "lat": 57.0670218,
+ "lng": 53.0277948
+ },
+ "Voronezh Oblast": {
+ "lat": 50.8589713,
+ "lng": 39.8644375
+ },
+ "Saratov Oblast": {
+ "lat": 51.83692629999999,
+ "lng": 46.7539397
+ },
+ "Belgorod Oblast": {
+ "lat": 50.71069259999999,
+ "lng": 37.7533377
+ },
+ "Kostroma Oblast": {
+ "lat": 58.5501069,
+ "lng": 43.9541103
+ },
+ "North Ossetia–Alania": {
+ "lat": 43.0451302,
+ "lng": 44.2870972
+ },
+ "Oryol oblast": {
+ "lat": 52.7856414,
+ "lng": 36.9242344
+ },
+ "Arkhangelskaya": {
+ "lat": 63.2852803,
+ "lng": 42.5884191
+ },
+ "Pskov Oblast": {
+ "lat": 56.7708599,
+ "lng": 29.094009
+ },
+ "Ryazan Oblast": {
+ "lat": 54.3875964,
+ "lng": 41.259566
+ },
+ "Bryansk Oblast": {
+ "lat": 53.0408599,
+ "lng": 33.2690899
+ },
+ "Ulyanovsk Oblast": {
+ "lat": 53.9793357,
+ "lng": 47.7762426
+ },
+ "Kabardino-Balkariya Republic": {
+ "lat": 43.3932469,
+ "lng": 43.5628498
+ },
+ "Ivanovo Oblast": {
+ "lat": 57.1056854,
+ "lng": 41.4830084
+ },
+ "Sverdlovsk Oblast": {
+ "lat": 59.007735,
+ "lng": 61.9316226
+ },
+ "Murmansk": {
+ "lat": 68.97331129999999,
+ "lng": 33.0855848
+ },
+ "Penza Oblast": {
+ "lat": 53.1412105,
+ "lng": 44.0940048
+ },
+ "Mordoviya Republic": {
+ "lat": 54.2369441,
+ "lng": 44.0683969
+ },
+ "Chechnya": {
+ "lat": 43.4023301,
+ "lng": 45.7187468
+ },
+ "Novgorod Oblast": {
+ "lat": 58.2427552,
+ "lng": 32.5665191
+ },
+ "Dagestan": {
+ "lat": 42.14318859999999,
+ "lng": 47.0949799
+ },
+ "Astrakhan Oblast": {
+ "lat": 46.1321166,
+ "lng": 48.0610115
+ },
+ "Ingushetiya Republic": {
+ "lat": 43.4051698,
+ "lng": 44.82029989999999
+ },
+ "Nenets": {
+ "lat": 67.6078337,
+ "lng": 57.63383309999999
+ },
+ "Kalmykiya Republic": {
+ "lat": 46.18671759999999,
+ "lng": 45
+ },
+ "Karachayevo-Cherkesiya Republic": {
+ "lat": 43.8845143,
+ "lng": 41.730394
+ },
+ "Kurgan Oblast": {
+ "lat": 55.4481548,
+ "lng": 65.11809749999999
+ },
+ "Tyumen Oblast": {
+ "lat": 56.9634387,
+ "lng": 66.948278
+ },
+ "Altai Krai": {
+ "lat": 51.7936298,
+ "lng": 82.6758596
+ },
+ "Krasnoyarsk Krai": {
+ "lat": 64.24797579999999,
+ "lng": 95.11041759999999
+ },
+ "Kemerovo Oblast": {
+ "lat": 54.7574648,
+ "lng": 87.4055288
+ },
+ "Yamalo-Nenets": {
+ "lat": 66.0653057,
+ "lng": 76.9345194
+ },
+ "Khakasiya Republic": {
+ "lat": 53.0452281,
+ "lng": 90.3982145
+ },
+ "Khanty-Mansia": {
+ "lat": 62.2287062,
+ "lng": 70.6410058
+ },
+ "Irkutsk Oblast": {
+ "lat": 56.13214199999999,
+ "lng": 103.948625
+ },
+ "Altai": {
+ "lat": 50.6181924,
+ "lng": 86.2199308
+ },
+ "Tomsk Oblast": {
+ "lat": 58.8969882,
+ "lng": 82.67654999999999
+ },
+ "Republic of Tyva": {
+ "lat": 51.8872669,
+ "lng": 95.62601719999999
+ },
+ "Omsk Oblast": {
+ "lat": 55.0554669,
+ "lng": 73.3167343
+ },
+ "Novosibirsk Oblast": {
+ "lat": 55.4467133,
+ "lng": 80.1043924
+ },
+ "Transbaikal Territory": {
+ "lat": 52.0471848,
+ "lng": 113.5176751
+ },
+ "Amur Oblast": {
+ "lat": 54.60350649999999,
+ "lng": 127.4801721
+ },
+ "Buryatiya Republic": {
+ "lat": 54.83311459999999,
+ "lng": 112.4060529
+ },
+ "Sakha": {
+ "lat": 66.7613451,
+ "lng": 124.1237531
+ },
+ "Primorye": {
+ "lat": 43.09164579999999,
+ "lng": 131.5873057
+ },
+ "Khabarovsk": {
+ "lat": 48.4814433,
+ "lng": 135.0720667
+ },
+ "Yevrey (Jewish) Autonomous Oblast": {
+ "lat": 48.4808147,
+ "lng": 131.7657367
+ },
+ "Kamchatka": {
+ "lat": 61.43439809999999,
+ "lng": 166.7884131
+ },
+ "Sakhalin Oblast": {
+ "lat": 49.9807847,
+ "lng": 143.3738129
+ },
+ "Magadan Oblast": {
+ "lat": 62.66434169999999,
+ "lng": 153.9149909
+ },
+ "Chukotka": {
+ "lat": 65.6298355,
+ "lng": 171.6952159
+ },
+ "": {
+ "lat": 61.52401,
+ "lng": 105.318756
+ }
+ },
+ "LV": {
+ "Valmiera": {
+ "lat": 57.53125290000001,
+ "lng": 25.4083677
+ },
+ "Saulkrasti Municipality": {
+ "lat": 57.2191743,
+ "lng": 24.5183794
+ },
+ "Ludza Municipality": {
+ "lat": 56.5446597,
+ "lng": 27.8386317
+ },
+ "Jelgava Municipality": {
+ "lat": 56.5032408,
+ "lng": 23.6926539
+ },
+ "Ropaži Municipality": {
+ "lat": 57.0034394,
+ "lng": 24.5155446
+ },
+ "Balvi Municipality": {
+ "lat": 57.0736222,
+ "lng": 27.4647376
+ },
+ "Jēkabpils Municipality": {
+ "lat": 56.2737577,
+ "lng": 25.8919513
+ },
+ "Limbaži Municipality": {
+ "lat": 57.7047514,
+ "lng": 24.6203033
+ },
+ "Bauska Municipality": {
+ "lat": 56.4152375,
+ "lng": 24.3533501
+ },
+ "Ventspils": {
+ "lat": 57.4088355,
+ "lng": 21.620621
+ },
+ "Varakļāni Municipality": {
+ "lat": 56.6100296,
+ "lng": 26.6391527
+ },
+ "Sigulda Municipality": {
+ "lat": 57.1327819,
+ "lng": 24.8162797
+ },
+ "Valka": {
+ "lat": 57.77526940000001,
+ "lng": 26.0211962
+ },
+ "Talsi Municipality": {
+ "lat": 57.3415208,
+ "lng": 22.5713125
+ },
+ "South Kurzeme Municipality": {
+ "lat": 56.57919949999999,
+ "lng": 21.5692299
+ },
+ "Tukums Municipality": {
+ "lat": 56.8555629,
+ "lng": 23.1166094
+ },
+ "Ogre": {
+ "lat": 56.8138328,
+ "lng": 24.6060417
+ },
+ "Cēsis Municipality": {
+ "lat": 57.1458187,
+ "lng": 25.4324729
+ },
+ "Smiltene Municipality": {
+ "lat": 57.39835829999999,
+ "lng": 26.0495451
+ },
+ "Kuldīga Municipality": {
+ "lat": 56.9052094,
+ "lng": 21.8522859
+ },
+ "Aizkraukle Municipality": {
+ "lat": 56.5283659,
+ "lng": 25.3921443
+ },
+ "Salaspils Municipality": {
+ "lat": 56.877881,
+ "lng": 24.3695603
+ },
+ "Saldus Municipality": {
+ "lat": 56.58679,
+ "lng": 22.5423817
+ },
+ "Riga": {
+ "lat": 56.9676941,
+ "lng": 24.1056221
+ },
+ "Rezekne": {
+ "lat": 56.5099129,
+ "lng": 27.3328284
+ },
+ "Ķekava": {
+ "lat": 56.8226783,
+ "lng": 24.2271889
+ },
+ "Preili Municipality": {
+ "lat": 56.3257394,
+ "lng": 26.7189095
+ },
+ "Mārupe": {
+ "lat": 56.9021321,
+ "lng": 24.0401848
+ },
+ "Ventspils Municipality": {
+ "lat": 57.2833682,
+ "lng": 21.8587558
+ },
+ "Madona Municipality": {
+ "lat": 56.8030323,
+ "lng": 26.1097605
+ },
+ "Līvāni": {
+ "lat": 56.3538262,
+ "lng": 26.1761923
+ },
+ "Liepaja": {
+ "lat": 56.5384577,
+ "lng": 21.0538204
+ },
+ "Augsdaugava Municipality": {
+ "lat": 55.9947059,
+ "lng": 26.2764871
+ },
+ "Dobele Municipality": {
+ "lat": 56.5333505,
+ "lng": 23.0638172
+ },
+ "Krāslava Municipality": {
+ "lat": 55.95655069999999,
+ "lng": 27.261882
+ },
+ "Ādaži": {
+ "lat": 57.07710789999999,
+ "lng": 24.323533
+ },
+ "Jurmala": {
+ "lat": 56.9684476,
+ "lng": 23.7737546
+ },
+ "Jelgava": {
+ "lat": 56.6511478,
+ "lng": 23.7196411
+ },
+ "Olaine": {
+ "lat": 56.7871834,
+ "lng": 23.9422997
+ },
+ "Gulbene Municipality": {
+ "lat": 57.2155645,
+ "lng": 26.6452955
+ },
+ "Daugavpils": {
+ "lat": 55.88261259999999,
+ "lng": 26.5464985
+ },
+ "Rēzekne Municipality": {
+ "lat": 56.3273638,
+ "lng": 27.3284332
+ },
+ "Alūksne Municipality": {
+ "lat": 57.3830775,
+ "lng": 27.1889735
+ },
+ "": {
+ "lat": 56.879635,
+ "lng": 24.603189
+ }
+ },
+ "EE": {
+ "Pärnumaa": {
+ "lat": 58.3916898,
+ "lng": 24.4952895
+ },
+ "Võrumaa": {
+ "lat": 57.84250170000001,
+ "lng": 27.0059657
+ },
+ "Ida-Virumaa": {
+ "lat": 59.25926629999999,
+ "lng": 27.4136535
+ },
+ "Viljandimaa": {
+ "lat": 58.363476,
+ "lng": 25.5859691
+ },
+ "Harjumaa": {
+ "lat": 59.33342390000001,
+ "lng": 25.2466973
+ },
+ "Lääne-Virumaa": {
+ "lat": 59.30188159999999,
+ "lng": 26.3280312
+ },
+ "Valgamaa": {
+ "lat": 57.78145679999999,
+ "lng": 26.0550403
+ },
+ "Tartu": {
+ "lat": 58.37798299999999,
+ "lng": 26.7290383
+ },
+ "Järvamaa": {
+ "lat": 58.8866713,
+ "lng": 25.5000625
+ },
+ "Lääne": {
+ "lat": 58.9722742,
+ "lng": 23.8740834
+ },
+ "Jõgevamaa": {
+ "lat": 58.7425969,
+ "lng": 26.3877205
+ },
+ "Raplamaa": {
+ "lat": 59.0000543,
+ "lng": 24.8048628
+ },
+ "Põlvamaa": {
+ "lat": 58.067018,
+ "lng": 27.0738741
+ },
+ "Saare": {
+ "lat": 57.9229168,
+ "lng": 22.0450493
+ },
+ "Hiiumaa": {
+ "lat": 58.9239553,
+ "lng": 22.5919468
+ },
+ "": {
+ "lat": 58.595272,
+ "lng": 25.013607
+ }
+ },
+ "LT": {
+ "Vilnius": {
+ "lat": 54.6871555,
+ "lng": 25.2796514
+ },
+ "Kaunas": {
+ "lat": 54.8985207,
+ "lng": 23.9035965
+ },
+ "Utena": {
+ "lat": 55.5000404,
+ "lng": 25.609385
+ },
+ "Siauliai": {
+ "lat": 55.9349085,
+ "lng": 23.3136823
+ },
+ "Klaipėda County": {
+ "lat": 55.6686983,
+ "lng": 21.4241373
+ },
+ "Marijampolė County": {
+ "lat": 54.78199710000001,
+ "lng": 23.1341364
+ },
+ "Telsiai": {
+ "lat": 55.9833874,
+ "lng": 22.2503539
+ },
+ "Alytus": {
+ "lat": 54.3960558,
+ "lng": 24.0460597
+ },
+ "Panevėžys": {
+ "lat": 55.7347915,
+ "lng": 24.3574773
+ },
+ "Tauragė County": {
+ "lat": 55.3072586,
+ "lng": 22.357294
+ },
+ "": {
+ "lat": 55.169438,
+ "lng": 23.881275
+ }
+ },
+ "UZ": {
+ "Karakalpakstan": {
+ "lat": 43.8041334,
+ "lng": 59.4457988
+ },
+ "Samarqand Region": {
+ "lat": 39.920791,
+ "lng": 66.4271499
+ },
+ "Qashqadaryo": {
+ "lat": 38.9271163,
+ "lng": 65.7539311
+ },
+ "Surxondaryo Region": {
+ "lat": 37.9409005,
+ "lng": 67.57085359999999
+ },
+ "Bukhara": {
+ "lat": 39.7680827,
+ "lng": 64.4555769
+ },
+ "Tashkent": {
+ "lat": 41.2994958,
+ "lng": 69.2400734
+ },
+ "Tashkent Region": {
+ "lat": 41.2213234,
+ "lng": 69.8597406
+ },
+ "Xorazm Region": {
+ "lat": 41.3565336,
+ "lng": 60.8566686
+ },
+ "Sirdaryo Region": {
+ "lat": 40.3863808,
+ "lng": 68.7154975
+ },
+ "Namangan": {
+ "lat": 41.0057729,
+ "lng": 71.6436028
+ },
+ "Navoiy Region": {
+ "lat": 42.6988575,
+ "lng": 64.6337685
+ },
+ "Jizzakh Region": {
+ "lat": 40.4706415,
+ "lng": 67.57085359999999
+ },
+ "Fergana": {
+ "lat": 40.37338020000001,
+ "lng": 71.7978333
+ },
+ "Andijan Region": {
+ "lat": 40.7685941,
+ "lng": 72.236379
+ },
+ "": {
+ "lat": 41.377491,
+ "lng": 64.585262
+ }
+ },
+ "SE": {
+ "Norrbotten County": {
+ "lat": 66.8309216,
+ "lng": 20.3991966
+ },
+ "Västerbotten County": {
+ "lat": 65.3337311,
+ "lng": 16.5161695
+ },
+ "Uppsala County": {
+ "lat": 60.2724233,
+ "lng": 18.1396124
+ },
+ "Jämtland County": {
+ "lat": 63.1711922,
+ "lng": 14.95918
+ },
+ "Västra Götaland County": {
+ "lat": 58.15991589999999,
+ "lng": 12.1360549
+ },
+ "Skåne County": {
+ "lat": 55.99025719999999,
+ "lng": 13.5957692
+ },
+ "Jönköping": {
+ "lat": 57.78261370000001,
+ "lng": 14.1617876
+ },
+ "Örebro County": {
+ "lat": 59.535036,
+ "lng": 15.0065731
+ },
+ "Östergötland County": {
+ "lat": 58.3453635,
+ "lng": 15.5197843
+ },
+ "Kronoberg County": {
+ "lat": 56.89064940000001,
+ "lng": 14.5084523
+ },
+ "Gotland County": {
+ "lat": 57.531291,
+ "lng": 18.6901396
+ },
+ "Kalmar": {
+ "lat": 56.6634447,
+ "lng": 16.356779
+ },
+ "Västmanland County": {
+ "lat": 59.6713879,
+ "lng": 16.2158954
+ },
+ "Södermanland County": {
+ "lat": 59.03363489999999,
+ "lng": 16.7518899
+ },
+ "Halland County": {
+ "lat": 56.7957699,
+ "lng": 12.893493
+ },
+ "Gävleborg County": {
+ "lat": 61.51322,
+ "lng": 15.53581
+ },
+ "Stockholm County": {
+ "lat": 59.4069048,
+ "lng": 18.8230665
+ },
+ "Dalarna County": {
+ "lat": 61.0917012,
+ "lng": 14.6663653
+ },
+ "Värmland County": {
+ "lat": 59.5807897,
+ "lng": 12.8472974
+ },
+ "Västernorrland County": {
+ "lat": 62.98552900000001,
+ "lng": 18.1258604
+ },
+ "Blekinge County": {
+ "lat": 56.143109,
+ "lng": 15.3666703
+ },
+ "": {
+ "lat": 60.128161,
+ "lng": 18.643501
+ }
+ },
+ "KZ": {
+ "Mangistauskaya Oblast'": {
+ "lat": 44.590802,
+ "lng": 53.84995079999999
+ },
+ "Atyrau Oblysy": {
+ "lat": 47.9053152,
+ "lng": 51.3780767
+ },
+ "West Kazakhstan": {
+ "lat": 49.56797270000001,
+ "lng": 50.8066617
+ },
+ "Aktyubinskaya Oblast'": {
+ "lat": 48.7797078,
+ "lng": 57.9974377
+ },
+ "Karaganda": {
+ "lat": 49.8046835,
+ "lng": 73.1093826
+ },
+ "Almaty": {
+ "lat": 43.2379761,
+ "lng": 76.8828618
+ },
+ "East Kazakhstan": {
+ "lat": 48.9131152,
+ "lng": 84.5574499
+ },
+ "Ulytau Region": {
+ "lat": 47.2847476,
+ "lng": 68.3533295
+ },
+ "Abai Region": {
+ "lat": 49.2237489,
+ "lng": 79.61572869999999
+ },
+ "Zhambyl Oblysy": {
+ "lat": 44.2220308,
+ "lng": 72.36579669999999
+ },
+ "North Kazakhstan": {
+ "lat": 54.1622066,
+ "lng": 69.9387071
+ },
+ "Aqmola Oblysy": {
+ "lat": 51.916532,
+ "lng": 69.4110493
+ },
+ "Jetisu Region": {
+ "lat": 45.0119227,
+ "lng": 78.4229392
+ },
+ "South Kazakhstan": {
+ "lat": 42.2663378,
+ "lng": 68.14314139999999
+ },
+ "Shymkent": {
+ "lat": 42.32053399999999,
+ "lng": 69.58763019999999
+ },
+ "Qostanay Oblysy": {
+ "lat": 51.5077096,
+ "lng": 64.04790729999999
+ },
+ "Qyzylorda Oblysy": {
+ "lat": 45.4114713,
+ "lng": 62.9526298
+ },
+ "Almaty Oblysy": {
+ "lat": 43.9368069,
+ "lng": 76.8259652
+ },
+ "Pavlodar Region": {
+ "lat": 52.65085440000001,
+ "lng": 76.7773224
+ },
+ "Astana": {
+ "lat": 51.1655126,
+ "lng": 71.4272222
+ },
+ "": {
+ "lat": 48.019573,
+ "lng": 66.923684
+ }
+ },
+ "GE": {
+ "Samegrelo and Zemo Svaneti": {
+ "lat": 42.7352247,
+ "lng": 42.1689362
+ },
+ "Mtskheta-Mtianeti": {
+ "lat": 42.16821849999999,
+ "lng": 44.6506057
+ },
+ "Shida Kartli": {
+ "lat": 42.0756944,
+ "lng": 43.9540462
+ },
+ "Kakheti": {
+ "lat": 41.6481602,
+ "lng": 45.6905554
+ },
+ "K'alak'i T'bilisi": {
+ "lat": 41.6938026,
+ "lng": 44.80151679999999
+ },
+ "Abkhazia": {
+ "lat": 42.9737816,
+ "lng": 41.4421799
+ },
+ "Imereti": {
+ "lat": 42.230108,
+ "lng": 42.9008665
+ },
+ "Kvemo Kartli": {
+ "lat": 41.4791833,
+ "lng": 44.65604510000001
+ },
+ "Guria": {
+ "lat": 41.9442736,
+ "lng": 42.0458091
+ },
+ "Racha-Lechkhumi and Kvemo Svaneti": {
+ "lat": 42.67188729999999,
+ "lng": 43.0562836
+ },
+ "Achara": {
+ "lat": 41.6005626,
+ "lng": 42.0688383
+ },
+ "Samtskhe-Javakheti": {
+ "lat": 41.5479296,
+ "lng": 43.2776399
+ },
+ "": {
+ "lat": 42.315407,
+ "lng": 43.356892
+ }
+ },
+ "UA": {
+ "Sumy": {
+ "lat": 50.9077,
+ "lng": 34.7981
+ },
+ "Donetsk": {
+ "lat": 48.015883,
+ "lng": 37.80285
+ },
+ "Lviv": {
+ "lat": 49.839683,
+ "lng": 24.029717
+ },
+ "Cherkasy Oblast": {
+ "lat": 49.444433,
+ "lng": 32.059767
+ },
+ "Luhansk": {
+ "lat": 48.574041,
+ "lng": 39.307815
+ },
+ "Kharkiv": {
+ "lat": 49.9935,
+ "lng": 36.230383
+ },
+ "Kirovohrad Oblast": {
+ "lat": 48.50793300000001,
+ "lng": 32.262317
+ },
+ "Zhytomyr": {
+ "lat": 50.25465,
+ "lng": 28.658667
+ },
+ "Vinnytsia": {
+ "lat": 49.233083,
+ "lng": 28.468217
+ },
+ "Khmelnytskyi Oblast": {
+ "lat": 49.422983,
+ "lng": 26.987133
+ },
+ "Dnipropetrovsk Oblast": {
+ "lat": 48.464717,
+ "lng": 35.046183
+ },
+ "Kyiv Oblast": {
+ "lat": 50.0529506,
+ "lng": 30.7667133
+ },
+ "Poltava Oblast": {
+ "lat": 49.58826699999999,
+ "lng": 34.551417
+ },
+ "Rivne": {
+ "lat": 50.6199,
+ "lng": 26.251617
+ },
+ "Ternopil Oblast": {
+ "lat": 49.553517,
+ "lng": 25.594767
+ },
+ "Zakarpattia Oblast": {
+ "lat": 48.6208,
+ "lng": 22.287883
+ },
+ "Volyn": {
+ "lat": 50.74723299999999,
+ "lng": 25.325383
+ },
+ "Chernivtsi": {
+ "lat": 48.2920787,
+ "lng": 25.9358367
+ },
+ "Zaporizhzhia": {
+ "lat": 47.8388,
+ "lng": 35.139567
+ },
+ "Ivano-Frankivsk Oblast": {
+ "lat": 48.922633,
+ "lng": 24.711117
+ },
+ "Odessa": {
+ "lat": 46.482526,
+ "lng": 30.7233095
+ },
+ "Crimea": {
+ "lat": 48.379433,
+ "lng": 31.16558
+ },
+ "Mykolaiv": {
+ "lat": 46.975033,
+ "lng": 31.994583
+ },
+ "Chernihiv": {
+ "lat": 51.4982,
+ "lng": 31.28935
+ },
+ "Kherson Oblast": {
+ "lat": 46.635417,
+ "lng": 32.616867
+ },
+ "Sebastopol City": {
+ "lat": 44.61665,
+ "lng": 33.525367
+ },
+ "Kyiv City": {
+ "lat": 50.4501,
+ "lng": 30.5234
+ },
+ "": {
+ "lat": 48.379433,
+ "lng": 31.16558
+ }
+ },
+ "MD": {
+ "Raionul Edineţ": {
+ "lat": 48.1678991,
+ "lng": 27.2936143
+ },
+ "Gagauzia": {
+ "lat": 46.0979435,
+ "lng": 28.6384645
+ },
+ "Nisporeni": {
+ "lat": 47.07513489999999,
+ "lng": 28.1768155
+ },
+ "Anenii Noi": {
+ "lat": 46.8795663,
+ "lng": 29.2312175
+ },
+ "Rîşcani": {
+ "lat": 47.9382017,
+ "lng": 27.5614647
+ },
+ "Chișinău Municipality": {
+ "lat": 47.0104529,
+ "lng": 28.8638103
+ },
+ "Ungheni": {
+ "lat": 47.2305767,
+ "lng": 27.7892661
+ },
+ "Taraclia": {
+ "lat": 45.898651,
+ "lng": 28.6671645
+ },
+ "Transnistria": {
+ "lat": 47.411631,
+ "lng": 28.369885
+ },
+ "Teleneşti": {
+ "lat": 47.4983962,
+ "lng": 28.3676019
+ },
+ "Cahul": {
+ "lat": 45.8939404,
+ "lng": 28.1890275
+ },
+ "Cantemir": {
+ "lat": 46.2771742,
+ "lng": 28.2009653
+ },
+ "Raionul Stefan Voda": {
+ "lat": 46.5540488,
+ "lng": 29.702242
+ },
+ "Briceni": {
+ "lat": 48.3632022,
+ "lng": 27.0750398
+ },
+ "Orhei": {
+ "lat": 47.38604,
+ "lng": 28.8303082
+ },
+ "Glodeni": {
+ "lat": 47.7790156,
+ "lng": 27.5168009
+ },
+ "Strășeni": {
+ "lat": 47.1450267,
+ "lng": 28.6136736
+ },
+ "Raionul Soroca": {
+ "lat": 48.1549743,
+ "lng": 28.2870783
+ },
+ "Şoldăneşti": {
+ "lat": 47.81473889999999,
+ "lng": 28.7889586
+ },
+ "Hînceşti": {
+ "lat": 46.8281147,
+ "lng": 28.5850889
+ },
+ "Raionul Causeni": {
+ "lat": 46.6554715,
+ "lng": 29.4091223
+ },
+ "Rezina": {
+ "lat": 47.7486542,
+ "lng": 28.9612499
+ },
+ "Făleşti": {
+ "lat": 47.5647725,
+ "lng": 27.7265593
+ },
+ "Sîngerei": {
+ "lat": 47.6389134,
+ "lng": 28.1371816
+ },
+ "Raionul Dubasari": {
+ "lat": 47.34481599999999,
+ "lng": 29.0765341
+ },
+ "Raionul Ocniţa": {
+ "lat": 48.3687818,
+ "lng": 27.4943835
+ },
+ "Criuleni": {
+ "lat": 47.2136114,
+ "lng": 29.1557519
+ },
+ "Floreşti": {
+ "lat": 47.8909785,
+ "lng": 28.298958
+ },
+ "Leova": {
+ "lat": 46.4780356,
+ "lng": 28.2539462
+ },
+ "Ialoveni": {
+ "lat": 46.9462689,
+ "lng": 28.7798944
+ },
+ "Drochia": {
+ "lat": 48.0318119,
+ "lng": 27.8182766
+ },
+ "Raionul Calarasi": {
+ "lat": 47.286946,
+ "lng": 28.274531
+ },
+ "Cimişlia": {
+ "lat": 46.5250851,
+ "lng": 28.7721835
+ },
+ "Bender Municipality": {
+ "lat": 46.84633609999999,
+ "lng": 29.4534416
+ },
+ "Basarabeasca": {
+ "lat": 46.3304439,
+ "lng": 28.9758649
+ },
+ "Municipiul Balti": {
+ "lat": 47.7778898,
+ "lng": 27.9087841
+ },
+ "": {
+ "lat": 47.411631,
+ "lng": 28.369885
+ }
+ },
+ "BY": {
+ "Minsk": {
+ "lat": 53.9006011,
+ "lng": 27.558972
+ },
+ "Homyel’ Voblasc’": {
+ "lat": 52.4313388,
+ "lng": 30.99367
+ },
+ "Brest": {
+ "lat": 52.0996507,
+ "lng": 23.7636662
+ },
+ "Vitebsk": {
+ "lat": 55.1926809,
+ "lng": 30.206359
+ },
+ "Grodnenskaya": {
+ "lat": 53.6599945,
+ "lng": 25.344857
+ },
+ "Mogilev": {
+ "lat": 53.8980663,
+ "lng": 30.3325337
+ },
+ "Minsk City": {
+ "lat": 53.9006011,
+ "lng": 27.558972
+ },
+ "": {
+ "lat": 53.709807,
+ "lng": 27.953389
+ }
+ },
+ "FI": {
+ "Ostrobothnia": {
+ "lat": 63.28620240000001,
+ "lng": 21.4765525
+ },
+ "Pirkanmaa": {
+ "lat": 61.6986918,
+ "lng": 23.7895598
+ },
+ "North Ostrobothnia": {
+ "lat": 64.7151463,
+ "lng": 26.018763
+ },
+ "Satakunta": {
+ "lat": 61.59327580000001,
+ "lng": 22.148308
+ },
+ "Southwest Finland": {
+ "lat": 60.36279139999999,
+ "lng": 22.4439369
+ },
+ "North Karelia": {
+ "lat": 62.8062078,
+ "lng": 30.1553887
+ },
+ "Kainuu": {
+ "lat": 64.3736564,
+ "lng": 28.7437475
+ },
+ "Kymenlaakso": {
+ "lat": 60.59772,
+ "lng": 27.2591176
+ },
+ "Paijat-Hame Region": {
+ "lat": 61.3230041,
+ "lng": 25.7322496
+ },
+ "Central Finland": {
+ "lat": 62.5666743,
+ "lng": 25.5549445
+ },
+ "Uusimaa": {
+ "lat": 60.21872,
+ "lng": 25.2716209
+ },
+ "North Savo": {
+ "lat": 62.94236739999999,
+ "lng": 27.7043601
+ },
+ "South Karelia Region": {
+ "lat": 61.2033481,
+ "lng": 28.6226138
+ },
+ "South Ostrobothnia": {
+ "lat": 62.9433099,
+ "lng": 23.5285266
+ },
+ "Kanta-Häme": {
+ "lat": 60.90701499999999,
+ "lng": 24.3005498
+ },
+ "Lapland": {
+ "lat": 67.9222304,
+ "lng": 26.5046438
+ },
+ "Southern Savonia": {
+ "lat": 61.6945148,
+ "lng": 27.8005015
+ },
+ "Central Ostrobothnia": {
+ "lat": 63.5621735,
+ "lng": 24.001363
+ },
+ "": {
+ "lat": 61.92411,
+ "lng": 25.748151
+ }
+ },
+ "RO": {
+ "Caras-Severin": {
+ "lat": 45.1139646,
+ "lng": 22.0740993
+ },
+ "Alba": {
+ "lat": 46.1558924,
+ "lng": 23.5556121
+ },
+ "Teleorman": {
+ "lat": 44.0160491,
+ "lng": 25.2986628
+ },
+ "Buzau": {
+ "lat": 45.1371109,
+ "lng": 26.8171122
+ },
+ "Hunedoara": {
+ "lat": 45.7678128,
+ "lng": 22.9072331
+ },
+ "Brasov": {
+ "lat": 45.6426802,
+ "lng": 25.5887252
+ },
+ "Neamt": {
+ "lat": 46.9758685,
+ "lng": 26.3818764
+ },
+ "Braila": {
+ "lat": 45.2652463,
+ "lng": 27.9594714
+ },
+ "Salaj": {
+ "lat": 47.2090813,
+ "lng": 23.2121901
+ },
+ "Sibiu": {
+ "lat": 45.80347889999999,
+ "lng": 24.1449997
+ },
+ "Arges": {
+ "lat": 45.0722527,
+ "lng": 24.8142726
+ },
+ "Mures": {
+ "lat": 46.5569904,
+ "lng": 24.6723215
+ },
+ "Mehedinti": {
+ "lat": 44.5515053,
+ "lng": 22.9044157
+ },
+ "Botosani": {
+ "lat": 47.7406537,
+ "lng": 26.6658127
+ },
+ "Ilfov": {
+ "lat": 44.535548,
+ "lng": 26.2324886
+ },
+ "Arad": {
+ "lat": 46.1865606,
+ "lng": 21.3122677
+ },
+ "Suceava": {
+ "lat": 47.6634521,
+ "lng": 26.2732302
+ },
+ "Iasi": {
+ "lat": 47.1584549,
+ "lng": 27.6014418
+ },
+ "Harghita": {
+ "lat": 46.4928507,
+ "lng": 25.6456696
+ },
+ "Dambovita": {
+ "lat": 44.9289893,
+ "lng": 25.425385
+ },
+ "Maramureş": {
+ "lat": 47.6737598,
+ "lng": 23.7456285
+ },
+ "Dolj": {
+ "lat": 44.1623022,
+ "lng": 23.6325054
+ },
+ "Galati": {
+ "lat": 45.4353208,
+ "lng": 28.0079945
+ },
+ "Vrancea": {
+ "lat": 45.81348759999999,
+ "lng": 27.0657531
+ },
+ "Vaslui": {
+ "lat": 46.6406915,
+ "lng": 27.7276468
+ },
+ "Calarasi": {
+ "lat": 44.2085144,
+ "lng": 27.3137439
+ },
+ "Timis": {
+ "lat": 45.7488716,
+ "lng": 21.2086793
+ },
+ "Constanta": {
+ "lat": 44.1759147,
+ "lng": 28.6519359
+ },
+ "Prahova": {
+ "lat": 45.08919059999999,
+ "lng": 26.0829312
+ },
+ "Bistrita-Nasaud": {
+ "lat": 47.2486107,
+ "lng": 24.5322814
+ },
+ "Bihor": {
+ "lat": 47.01575159999999,
+ "lng": 22.172266
+ },
+ "Bacau": {
+ "lat": 46.5670437,
+ "lng": 26.9145748
+ },
+ "Giurgiu": {
+ "lat": 43.9037076,
+ "lng": 25.9699265
+ },
+ "Ialomita": {
+ "lat": 44.603133,
+ "lng": 27.3789914
+ },
+ "Covasna": {
+ "lat": 45.84727669999999,
+ "lng": 26.1783452
+ },
+ "Cluj": {
+ "lat": 46.7712101,
+ "lng": 23.6236353
+ },
+ "Tulcea": {
+ "lat": 45.1716165,
+ "lng": 28.7914439
+ },
+ "Gorj": {
+ "lat": 44.94855949999999,
+ "lng": 23.2427079
+ },
+ "Olt": {
+ "lat": 44.200797,
+ "lng": 24.5022981
+ },
+ "Satu Mare": {
+ "lat": 47.8016702,
+ "lng": 22.8575926
+ },
+ "Valcea": {
+ "lat": 45.30095439999999,
+ "lng": 24.1590342
+ },
+ "Bucuresti": {
+ "lat": 44.4267674,
+ "lng": 26.1025384
+ },
+ "": {
+ "lat": 45.943161,
+ "lng": 24.96676
+ }
+ },
+ "HU": {
+ "Hajdú-Bihar": {
+ "lat": 47.4688355,
+ "lng": 21.5453228
+ },
+ "Bekes County": {
+ "lat": 46.67048990000001,
+ "lng": 21.0434996
+ },
+ "Szabolcs-Szatmár-Bereg": {
+ "lat": 48.0394954,
+ "lng": 22.00333
+ },
+ "Jász-Nagykun-Szolnok": {
+ "lat": 47.2555579,
+ "lng": 20.5232456
+ },
+ "Heves megye": {
+ "lat": 47.8057617,
+ "lng": 20.2038559
+ },
+ "Csongrad megye": {
+ "lat": 46.416705,
+ "lng": 20.2566161
+ },
+ "Borsod-Abaúj-Zemplén": {
+ "lat": 48.2939401,
+ "lng": 20.6934113
+ },
+ "Bács-Kiskun": {
+ "lat": 46.5661437,
+ "lng": 19.4272464
+ },
+ "Pest megye": {
+ "lat": 47.44800009999999,
+ "lng": 19.4618128
+ },
+ "Budapest": {
+ "lat": 47.497912,
+ "lng": 19.040235
+ },
+ "Somogy megye": {
+ "lat": 46.554859,
+ "lng": 17.5866732
+ },
+ "Veszprem megye": {
+ "lat": 47.0930974,
+ "lng": 17.9100763
+ },
+ "Fejér": {
+ "lat": 47.1217932,
+ "lng": 18.5294815
+ },
+ "Zala": {
+ "lat": 46.73844039999999,
+ "lng": 16.9152252
+ },
+ "Győr-Moson-Sopron": {
+ "lat": 47.6509285,
+ "lng": 17.2505883
+ },
+ "Komárom-Esztergom": {
+ "lat": 47.7390852,
+ "lng": 18.1267006
+ },
+ "Vas": {
+ "lat": 47.09291109999999,
+ "lng": 16.6812183
+ },
+ "Baranya": {
+ "lat": 46.0484585,
+ "lng": 18.2719173
+ },
+ "Nograd megye": {
+ "lat": 47.9218427,
+ "lng": 19.5586447
+ },
+ "Tolna megye": {
+ "lat": 46.4762754,
+ "lng": 18.5570627
+ },
+ "": {
+ "lat": 47.162494,
+ "lng": 19.503304
+ }
+ },
+ "SK": {
+ "Presov": {
+ "lat": 49.0018324,
+ "lng": 21.2393119
+ },
+ "Kosice": {
+ "lat": 48.7163857,
+ "lng": 21.2610746
+ },
+ "Banska Bystrica": {
+ "lat": 48.736277,
+ "lng": 19.1461917
+ },
+ "Zilina": {
+ "lat": 49.21944980000001,
+ "lng": 18.7408001
+ },
+ "Trencin": {
+ "lat": 48.884936,
+ "lng": 18.0335209
+ },
+ "Nitra": {
+ "lat": 48.3061414,
+ "lng": 18.076376
+ },
+ "Trnava": {
+ "lat": 48.3943898,
+ "lng": 17.7216205
+ },
+ "Bratislava": {
+ "lat": 48.1485965,
+ "lng": 17.1077477
+ },
+ "": {
+ "lat": 48.669026,
+ "lng": 19.699024
+ }
+ },
+ "BG": {
+ "Varna": {
+ "lat": 43.2140504,
+ "lng": 27.9147333
+ },
+ "Dobrich": {
+ "lat": 43.57259,
+ "lng": 27.8272606
+ },
+ "Kardzhali": {
+ "lat": 41.6338439,
+ "lng": 25.3777119
+ },
+ "Smolyan": {
+ "lat": 41.5774233,
+ "lng": 24.7011138
+ },
+ "Sofia": {
+ "lat": 42.6977082,
+ "lng": 23.3218675
+ },
+ "Plovdiv": {
+ "lat": 42.1354079,
+ "lng": 24.7452904
+ },
+ "Veliko Tarnovo": {
+ "lat": 43.0756739,
+ "lng": 25.6171514
+ },
+ "Blagoevgrad": {
+ "lat": 42.0208569,
+ "lng": 23.0943385
+ },
+ "Kyustendil": {
+ "lat": 42.2868817,
+ "lng": 22.6939308
+ },
+ "Vratsa": {
+ "lat": 43.2102375,
+ "lng": 23.5528803
+ },
+ "Burgas": {
+ "lat": 42.50479259999999,
+ "lng": 27.4626361
+ },
+ "Silistra": {
+ "lat": 44.1147271,
+ "lng": 27.2671901
+ },
+ "Razgrad": {
+ "lat": 43.53367189999999,
+ "lng": 26.5411164
+ },
+ "Pazardzhik": {
+ "lat": 42.1927654,
+ "lng": 24.3335662
+ },
+ "Pleven": {
+ "lat": 43.4170423,
+ "lng": 24.6066847
+ },
+ "Yambol": {
+ "lat": 42.48419990000001,
+ "lng": 26.5035023
+ },
+ "Sliven": {
+ "lat": 42.6816536,
+ "lng": 26.3228685
+ },
+ "Lovech": {
+ "lat": 43.1369534,
+ "lng": 24.7141906
+ },
+ "Montana": {
+ "lat": 43.4085161,
+ "lng": 23.2257292
+ },
+ "Sofia-Capital": {
+ "lat": 42.7570109,
+ "lng": 23.4504683
+ },
+ "Vidin": {
+ "lat": 43.996159,
+ "lng": 22.8679302
+ },
+ "Ruse": {
+ "lat": 43.83557130000001,
+ "lng": 25.9656554
+ },
+ "Haskovo": {
+ "lat": 41.9344366,
+ "lng": 25.5554462
+ },
+ "Stara Zagora": {
+ "lat": 42.4257769,
+ "lng": 25.6344644
+ },
+ "Targovishte": {
+ "lat": 43.2493556,
+ "lng": 26.5727357
+ },
+ "Gabrovo": {
+ "lat": 42.8742212,
+ "lng": 25.3186837
+ },
+ "Pernik": {
+ "lat": 42.6051862,
+ "lng": 23.0378368
+ },
+ "Shumen": {
+ "lat": 43.2712398,
+ "lng": 26.9361286
+ },
+ "": {
+ "lat": 42.733883,
+ "lng": 25.48583
+ }
+ },
+ "PL": {
+ "Lublin": {
+ "lat": 51.2464536,
+ "lng": 22.5684463
+ },
+ "Mazovia": {
+ "lat": 52.5373228,
+ "lng": 21.0768288
+ },
+ "Subcarpathia": {
+ "lat": 50.0574749,
+ "lng": 22.0895691
+ },
+ "Świętokrzyskie": {
+ "lat": 50.6261041,
+ "lng": 20.9406279
+ },
+ "Lesser Poland": {
+ "lat": 49.72253060000001,
+ "lng": 20.2503359
+ },
+ "Warmia-Masuria": {
+ "lat": 53.8671117,
+ "lng": 20.702786
+ },
+ "Podlasie": {
+ "lat": 53.0697159,
+ "lng": 22.9674639
+ },
+ "Łódź Voivodeship": {
+ "lat": 51.4634771,
+ "lng": 19.1726974
+ },
+ "Silesia": {
+ "lat": 50.5716595,
+ "lng": 19.3219768
+ },
+ "Opole Voivodeship": {
+ "lat": 50.8003761,
+ "lng": 17.937989
+ },
+ "West Pomerania": {
+ "lat": 53.46578909999999,
+ "lng": 15.1822581
+ },
+ "Lubusz": {
+ "lat": 52.2274612,
+ "lng": 15.2559103
+ },
+ "Pomerania": {
+ "lat": 54.2944252,
+ "lng": 18.1531164
+ },
+ "Lower Silesia": {
+ "lat": 51.13398609999999,
+ "lng": 16.8841961
+ },
+ "Kujawsko-Pomorskie": {
+ "lat": 53.1648363,
+ "lng": 18.4834224
+ },
+ "Greater Poland": {
+ "lat": 52.279986,
+ "lng": 17.3522939
+ },
+ "": {
+ "lat": 51.919438,
+ "lng": 19.145136
+ }
+ },
+ "NO": {
+ "Troms og Finnmark": {
+ "lat": 70.0718208,
+ "lng": 23.2171664
+ },
+ "Nordland": {
+ "lat": 68.6670601,
+ "lng": 14.946742
+ },
+ "Vestland": {
+ "lat": 60.8138379,
+ "lng": 6.1410712
+ },
+ "Viken": {
+ "lat": 60.0389059,
+ "lng": 9.725371899999999
+ },
+ "Møre og Romsdal": {
+ "lat": 62.0743203,
+ "lng": 6.2108624
+ },
+ "Rogaland": {
+ "lat": 59.1489544,
+ "lng": 6.0143431
+ },
+ "Innlandet": {
+ "lat": 61.3368595,
+ "lng": 9.946462799999999
+ },
+ "Trøndelag": {
+ "lat": 63.4305149,
+ "lng": 10.3950528
+ },
+ "Agder": {
+ "lat": 58.50320840000001,
+ "lng": 7.757197499999999
+ },
+ "Vestfold og Telemark": {
+ "lat": 59.35421950000001,
+ "lng": 8.8817981
+ },
+ "Oslo County": {
+ "lat": 59.9138688,
+ "lng": 10.7522454
+ },
+ "": {
+ "lat": 60.472024,
+ "lng": 8.468946
+ }
+ },
+ "MK": {
+ "Zrnovci": {
+ "lat": 41.854056,
+ "lng": 22.4438316
+ },
+ "Zhelino": {
+ "lat": 41.98034190000001,
+ "lng": 21.0609438
+ },
+ "Vinica": {
+ "lat": 41.883306,
+ "lng": 22.5081193
+ },
+ "Vevchani": {
+ "lat": 41.24075430000001,
+ "lng": 20.5915649
+ },
+ "Vasilevo": {
+ "lat": 41.4741699,
+ "lng": 22.6422128
+ },
+ "Valandovo": {
+ "lat": 41.3169713,
+ "lng": 22.5617585
+ },
+ "Veles": {
+ "lat": 41.7164563,
+ "lng": 21.7722966
+ },
+ "Tetovo": {
+ "lat": 42.0069115,
+ "lng": 20.9715269
+ },
+ "Tearce": {
+ "lat": 42.0777511,
+ "lng": 21.0534923
+ },
+ "Sveti Nikole": {
+ "lat": 41.8656094,
+ "lng": 21.9373445
+ },
+ "Strumica": {
+ "lat": 41.4378004,
+ "lng": 22.6427428
+ },
+ "Struga": {
+ "lat": 41.1778353,
+ "lng": 20.6783258
+ },
+ "Shtip": {
+ "lat": 41.746429,
+ "lng": 22.199654
+ },
+ "Petrovec": {
+ "lat": 41.9401891,
+ "lng": 21.6094324
+ },
+ "Debarca": {
+ "lat": 41.3584077,
+ "lng": 20.8552919
+ },
+ "Mavrovo and Rostuša": {
+ "lat": 41.6243153,
+ "lng": 20.6645683
+ },
+ "Rosoman": {
+ "lat": 41.5175986,
+ "lng": 21.9433064
+ },
+ "Resen": {
+ "lat": 41.0902853,
+ "lng": 21.0132542
+ },
+ "Rankovce": {
+ "lat": 42.1695387,
+ "lng": 22.116195
+ },
+ "Radovish": {
+ "lat": 41.6395303,
+ "lng": 22.4678884
+ },
+ "Probishtip": {
+ "lat": 41.9948176,
+ "lng": 22.1877315
+ },
+ "Prilep": {
+ "lat": 41.3440827,
+ "lng": 21.5527922
+ },
+ "Plasnica": {
+ "lat": 41.471841,
+ "lng": 21.1220478
+ },
+ "Ohrid": {
+ "lat": 41.1230977,
+ "lng": 20.8016481
+ },
+ "Novo Selo": {
+ "lat": 41.4117903,
+ "lng": 22.87907
+ },
+ "Pehchevo": {
+ "lat": 41.7621467,
+ "lng": 22.8865173
+ },
+ "Negotino": {
+ "lat": 41.48292199999999,
+ "lng": 22.092349
+ },
+ "Demir Hisar": {
+ "lat": 41.22136039999999,
+ "lng": 21.2025287
+ },
+ "Mogila": {
+ "lat": 41.15723939999999,
+ "lng": 21.4037407
+ },
+ "Vrapchishte": {
+ "lat": 41.83841779999999,
+ "lng": 20.8858003
+ },
+ "Opstina Centar": {
+ "lat": 41.9953628,
+ "lng": 21.4246078
+ },
+ "Kumanovo": {
+ "lat": 42.0732613,
+ "lng": 21.7853143
+ },
+ "Kriva Palanka": {
+ "lat": 42.2058454,
+ "lng": 22.3307965
+ },
+ "Kratovo": {
+ "lat": 42.0800379,
+ "lng": 22.1802799
+ },
+ "Saraj": {
+ "lat": 41.9994052,
+ "lng": 21.324745
+ },
+ "Kochani": {
+ "lat": 41.91680480000001,
+ "lng": 22.4082849
+ },
+ "Kichevo": {
+ "lat": 41.5129112,
+ "lng": 20.9525065
+ },
+ "Kavadarci": {
+ "lat": 41.4329364,
+ "lng": 22.0088861
+ },
+ "Karbinci": {
+ "lat": 41.8180159,
+ "lng": 22.2324758
+ },
+ "Makedonska Kamenica": {
+ "lat": 42.0214425,
+ "lng": 22.5870873
+ },
+ "Jegunovce": {
+ "lat": 42.07407200000001,
+ "lng": 21.1220478
+ },
+ "Gradsko": {
+ "lat": 41.5750625,
+ "lng": 21.9494531
+ },
+ "Gostivar": {
+ "lat": 41.80255409999999,
+ "lng": 20.9089378
+ },
+ "Karposh": {
+ "lat": 42.0029617,
+ "lng": 21.3977787
+ },
+ "Centar Zhupa": {
+ "lat": 41.47852779999999,
+ "lng": 20.5602793
+ },
+ "Gevgelija": {
+ "lat": 41.1451892,
+ "lng": 22.4997467
+ },
+ "Konche": {
+ "lat": 41.5171011,
+ "lng": 22.3814624
+ },
+ "Lozovo": {
+ "lat": 41.7818139,
+ "lng": 21.9000827
+ },
+ "Gjorche Petrov": {
+ "lat": 42.0069781,
+ "lng": 21.3649879
+ },
+ "Dolneni": {
+ "lat": 41.42701539999999,
+ "lng": 21.4529274
+ },
+ "Demir Kapija": {
+ "lat": 41.4088425,
+ "lng": 22.2436177
+ },
+ "Zelenikovo": {
+ "lat": 41.8831336,
+ "lng": 21.5893102
+ },
+ "Debar": {
+ "lat": 41.51975729999999,
+ "lng": 20.5289
+ },
+ "Makedonski Brod": {
+ "lat": 41.5133088,
+ "lng": 21.2174329
+ },
+ "Bogovinje": {
+ "lat": 41.9236371,
+ "lng": 20.9163887
+ },
+ "Bogdanci": {
+ "lat": 41.203138,
+ "lng": 22.5754421
+ },
+ "Bitola": {
+ "lat": 41.0296773,
+ "lng": 21.3292164
+ },
+ "Berovo": {
+ "lat": 41.7060632,
+ "lng": 22.8552379
+ },
+ "Studenichani": {
+ "lat": 41.9225639,
+ "lng": 21.5363965
+ },
+ "Butel": {
+ "lat": 42.0297408,
+ "lng": 21.4424938
+ },
+ "Chucher Sandevo": {
+ "lat": 42.0974773,
+ "lng": 21.3877179
+ },
+ "Kisela Voda": {
+ "lat": 41.9747129,
+ "lng": 21.4454748
+ },
+ "Arachinovo": {
+ "lat": 42.02538,
+ "lng": 21.5639736
+ },
+ "Aerodrom": {
+ "lat": 41.9795248,
+ "lng": 21.469323
+ },
+ "": {
+ "lat": 41.608635,
+ "lng": 21.745275
+ }
+ },
+ "RS": {
+ "Vojvodina": {
+ "lat": 45.2608651,
+ "lng": 19.8319339
+ },
+ "Belgrade": {
+ "lat": 44.8125449,
+ "lng": 20.46123
+ },
+ "Zajecar": {
+ "lat": 43.9018089,
+ "lng": 22.27015
+ },
+ "Raska": {
+ "lat": 43.2863993,
+ "lng": 20.6150936
+ },
+ "Pcinja": {
+ "lat": 42.5836362,
+ "lng": 22.1430215
+ },
+ "Jablanica": {
+ "lat": 43.4530694,
+ "lng": 21.2688456
+ },
+ "Branicevo": {
+ "lat": 44.7046041,
+ "lng": 21.5371328
+ },
+ "Podunavlje": {
+ "lat": 44.4729156,
+ "lng": 20.9901426
+ },
+ "Kolubara": {
+ "lat": 44.3509811,
+ "lng": 20.0004305
+ },
+ "Rasina": {
+ "lat": 43.5263525,
+ "lng": 21.1588179
+ },
+ "Morava": {
+ "lat": 44.20574536720186,
+ "lng": 21.22756176735834
+ },
+ "Sumadija": {
+ "lat": 44.20506779999999,
+ "lng": 20.7856565
+ },
+ "Nisava": {
+ "lat": 43.3738902,
+ "lng": 21.9322331
+ },
+ "Pomoravlje": {
+ "lat": 43.95913789999999,
+ "lng": 21.271353
+ },
+ "Toplica": {
+ "lat": 43.1906592,
+ "lng": 21.3407762
+ },
+ "Zlatibor": {
+ "lat": 43.7304492,
+ "lng": 19.6982165
+ },
+ "Bor": {
+ "lat": 44.0698918,
+ "lng": 22.0985086
+ },
+ "Pirot": {
+ "lat": 43.1570535,
+ "lng": 22.5839865
+ },
+ "Macva": {
+ "lat": 44.5925314,
+ "lng": 19.5082246
+ },
+ "": {
+ "lat": 44.016521,
+ "lng": 21.005859
+ }
+ },
+ "ME": {
+ "Rožaje Municipality": {
+ "lat": 42.8487304,
+ "lng": 20.1879106
+ },
+ "Opstina Zabljak": {
+ "lat": 43.09381399999999,
+ "lng": 19.2355764
+ },
+ "Ulcinj": {
+ "lat": 41.9310884,
+ "lng": 19.2147632
+ },
+ "Tuzi": {
+ "lat": 42.36894239999999,
+ "lng": 19.3128909
+ },
+ "Tivat": {
+ "lat": 42.4319094,
+ "lng": 18.6986213
+ },
+ "Danilovgrad": {
+ "lat": 42.5537532,
+ "lng": 19.1077389
+ },
+ "Opstina Savnik": {
+ "lat": 42.9603756,
+ "lng": 19.140438
+ },
+ "Kotor": {
+ "lat": 42.424662,
+ "lng": 18.771234
+ },
+ "Podgorica": {
+ "lat": 42.4304196,
+ "lng": 19.2593642
+ },
+ "Opstina Pluzine": {
+ "lat": 43.1593384,
+ "lng": 18.8551484
+ },
+ "Pljevlja": {
+ "lat": 43.3582371,
+ "lng": 19.3512591
+ },
+ "Opstina Plav": {
+ "lat": 42.60598419999999,
+ "lng": 19.9735048
+ },
+ "Budva": {
+ "lat": 42.2911489,
+ "lng": 18.840295
+ },
+ "Opstina Niksic": {
+ "lat": 42.7997184,
+ "lng": 18.7600963
+ },
+ "Mojkovac": {
+ "lat": 42.9600774,
+ "lng": 19.5835874
+ },
+ "Opstina Kolasin": {
+ "lat": 42.7601916,
+ "lng": 19.4259114
+ },
+ "Berane": {
+ "lat": 42.83793439999999,
+ "lng": 19.8603732
+ },
+ "Herceg Novi": {
+ "lat": 42.4572478,
+ "lng": 18.5314753
+ },
+ "Gusinje": {
+ "lat": 42.5619739,
+ "lng": 19.8320935
+ },
+ "Cetinje": {
+ "lat": 42.3930959,
+ "lng": 18.9115964
+ },
+ "Bijelo Polje": {
+ "lat": 43.0369422,
+ "lng": 19.7561911
+ },
+ "Bar": {
+ "lat": 42.0912106,
+ "lng": 19.089904
+ },
+ "Petnjica": {
+ "lat": 42.9069016,
+ "lng": 19.9586179
+ },
+ "": {
+ "lat": 42.708678,
+ "lng": 19.37439
+ }
+ },
+ "NA": {
+ "Zambezi Region": {
+ "lat": -17.8193419,
+ "lng": 23.9536466
+ },
+ "Khomas": {
+ "lat": -22.6377854,
+ "lng": 17.1011931
+ },
+ "Oshikoto": {
+ "lat": -18.4152575,
+ "lng": 16.912251
+ },
+ "Erongo": {
+ "lat": -22.2565682,
+ "lng": 15.4068079
+ },
+ "Kavango East": {
+ "lat": -18.271048,
+ "lng": 18.4276047
+ },
+ "Kunene": {
+ "lat": -19.4086317,
+ "lng": 13.914399
+ },
+ "Otjozondjupa": {
+ "lat": -20.5486916,
+ "lng": 17.668887
+ },
+ "Oshana": {
+ "lat": -18.4305064,
+ "lng": 15.6881788
+ },
+ "Karas": {
+ "lat": -26.8429645,
+ "lng": 17.2902839
+ },
+ "Omusati": {
+ "lat": -18.4070294,
+ "lng": 14.8454619
+ },
+ "Kavango West": {
+ "lat": -18.271048,
+ "lng": 18.4276047
+ },
+ "Hardap": {
+ "lat": -24.533333,
+ "lng": 17.933333
+ },
+ "Ohangwena": {
+ "lat": -17.5979291,
+ "lng": 16.8178377
+ },
+ "": {
+ "lat": -22.95764,
+ "lng": 18.49041
+ }
+ },
+ "ZW": {
+ "Matabeleland North": {
+ "lat": -18.5331566,
+ "lng": 27.5495846
+ },
+ "Manicaland": {
+ "lat": -18.9216386,
+ "lng": 32.174605
+ },
+ "Mashonaland East Province": {
+ "lat": -18.5871642,
+ "lng": 31.2626366
+ },
+ "Masvingo Province": {
+ "lat": -20.6241509,
+ "lng": 31.2626366
+ },
+ "Midlands Province": {
+ "lat": -19.0552009,
+ "lng": 29.6035495
+ },
+ "Mashonaland West": {
+ "lat": -17.4851029,
+ "lng": 29.7889248
+ },
+ "Harare": {
+ "lat": -17.8216288,
+ "lng": 31.0492259
+ },
+ "Matabeleland South Province": {
+ "lat": -21.052337,
+ "lng": 29.0459927
+ },
+ "Bulawayo": {
+ "lat": -20.1457125,
+ "lng": 28.5873388
+ },
+ "": {
+ "lat": -19.015438,
+ "lng": 29.154857
+ }
+ },
+ "KM": {
+ "Mohéli": {
+ "lat": -12.3377376,
+ "lng": 43.7334089
+ },
+ "Ndzuwani": {
+ "lat": -12.2138145,
+ "lng": 44.4370606
+ },
+ "Grande Comore": {
+ "lat": -11.7167338,
+ "lng": 43.3680788
+ },
+ "": {
+ "lat": -11.875001,
+ "lng": 43.872219
+ }
+ },
+ "MW": {
+ "Central Region": {
+ "lat": -13.4830679,
+ "lng": 34.1531947
+ },
+ "Northern Region": {
+ "lat": -10.9625469,
+ "lng": 34.1531947
+ },
+ "Southern Region": {
+ "lat": -15.4201189,
+ "lng": 35.0388164
+ },
+ "": {
+ "lat": -13.254308,
+ "lng": 34.301525
+ }
+ },
+ "LS": {
+ "Berea": {
+ "lat": -29.2068943,
+ "lng": 27.8780275
+ },
+ "Quthing": {
+ "lat": -30.4015687,
+ "lng": 27.7080133
+ },
+ "Qacha's Nek": {
+ "lat": -30.1114565,
+ "lng": 28.678979
+ },
+ "Leribe": {
+ "lat": -28.8638065,
+ "lng": 28.0478826
+ },
+ "Mokhotlong": {
+ "lat": -29.2875557,
+ "lng": 29.06053889999999
+ },
+ "Mohale's Hoek District": {
+ "lat": -30.0812662,
+ "lng": 27.784246
+ },
+ "Maseru": {
+ "lat": -29.3150767,
+ "lng": 27.4869229
+ },
+ "Mafeteng District": {
+ "lat": -29.7606683,
+ "lng": 27.3146351
+ },
+ "Butha-Buthe": {
+ "lat": -28.7653754,
+ "lng": 28.2468148
+ },
+ "Thaba-Tseka": {
+ "lat": -29.5238975,
+ "lng": 28.6089752
+ },
+ "": {
+ "lat": -29.609988,
+ "lng": 28.233608
+ }
+ },
+ "BW": {
+ "Kgalagadi District": {
+ "lat": -24.7550285,
+ "lng": 21.8568586
+ },
+ "South-East": {
+ "lat": -24.9630286,
+ "lng": 25.7811869
+ },
+ "Kweneng District": {
+ "lat": -23.8367249,
+ "lng": 25.2837585
+ },
+ "Central District": {
+ "lat": -21.5831595,
+ "lng": 26.0413889
+ },
+ "Selibe Phikwe": {
+ "lat": -21.9760747,
+ "lng": 27.837023
+ },
+ "Kgatleng District": {
+ "lat": -24.1970445,
+ "lng": 26.2304616
+ },
+ "North-West": {
+ "lat": -19.9070787,
+ "lng": 23.0011989
+ },
+ "Lobatse": {
+ "lat": -25.2076148,
+ "lng": 25.6805868
+ },
+ "Chobe District": {
+ "lat": -18.4256281,
+ "lng": 24.7142918
+ },
+ "Ngwaketsi": {
+ "lat": -25.0405342,
+ "lng": 25.2363408
+ },
+ "Gaborone": {
+ "lat": -24.6282079,
+ "lng": 25.9231471
+ },
+ "City of Francistown": {
+ "lat": -21.1661005,
+ "lng": 27.5143603
+ },
+ "Ghanzi District": {
+ "lat": -21.8652314,
+ "lng": 21.8568586
+ },
+ "Sowa Town": {
+ "lat": -20.5653345,
+ "lng": 26.2235167
+ },
+ "Jwaneng": {
+ "lat": -24.6059582,
+ "lng": 24.7202287
+ },
+ "": {
+ "lat": -22.328474,
+ "lng": 24.684866
+ }
+ },
+ "MU": {
+ "Grand Port District": {
+ "lat": -20.3851546,
+ "lng": 57.6665742
+ },
+ "Moka District": {
+ "lat": -20.2399782,
+ "lng": 57.57592599999999
+ },
+ "Plaines Wilhems District": {
+ "lat": -20.3054872,
+ "lng": 57.48535609999999
+ },
+ "Flacq District": {
+ "lat": -20.2257836,
+ "lng": 57.7119274
+ },
+ "Pamplemousses District": {
+ "lat": -20.1094972,
+ "lng": 57.5815892
+ },
+ "Riviere du Rempart District": {
+ "lat": -20.0560983,
+ "lng": 57.65523890000001
+ },
+ "Black River District": {
+ "lat": -20.3708492,
+ "lng": 57.39486489999999
+ },
+ "Savanne District": {
+ "lat": -20.473953,
+ "lng": 57.48535609999999
+ },
+ "Agalega Islands": {
+ "lat": -10.4681988,
+ "lng": 56.690672
+ },
+ "Port Louis District": {
+ "lat": -20.1608912,
+ "lng": 57.5012222
+ },
+ "Rodrigues": {
+ "lat": -19.7245385,
+ "lng": 63.4272185
+ },
+ "Cargados Carajos": {
+ "lat": -16.583333,
+ "lng": 59.61666699999999
+ },
+ "": {
+ "lat": -20.348404,
+ "lng": 57.552152
+ }
+ },
+ "SZ": {
+ "Lubombo District": {
+ "lat": -26.7851773,
+ "lng": 31.8107079
+ },
+ "Shiselweni District": {
+ "lat": -26.9827577,
+ "lng": 31.3541631
+ },
+ "Hhohho": {
+ "lat": -26.1365662,
+ "lng": 31.3541631
+ },
+ "Manzini": {
+ "lat": -26.5081999,
+ "lng": 31.3713164
+ },
+ "": {
+ "lat": -26.522503,
+ "lng": 31.465866
+ }
+ },
+ "ZA": {
+ "Gauteng": {
+ "lat": -26.2707593,
+ "lng": 28.1122679
+ },
+ "North West": {
+ "lat": -26.6638599,
+ "lng": 25.2837585
+ },
+ "Limpopo": {
+ "lat": -23.4012946,
+ "lng": 29.4179324
+ },
+ "Orange Free State": {
+ "lat": -28.4541105,
+ "lng": 26.7967849
+ },
+ "Mpumalanga": {
+ "lat": -25.565336,
+ "lng": 30.5279096
+ },
+ "KwaZulu-Natal": {
+ "lat": -28.5305539,
+ "lng": 30.8958242
+ },
+ "Eastern Cape": {
+ "lat": -32.2968402,
+ "lng": 26.419389
+ },
+ "Northern Cape": {
+ "lat": -29.0466808,
+ "lng": 21.8568586
+ },
+ "Western Cape": {
+ "lat": -33.2277918,
+ "lng": 21.8568586
+ },
+ "": {
+ "lat": -30.559482,
+ "lng": 22.937506
+ }
+ },
+ "MZ": {
+ "Gaza Province": {
+ "lat": -23.0221928,
+ "lng": 32.7181375
+ },
+ "Inhambane Province": {
+ "lat": -22.6830732,
+ "lng": 34.1531947
+ },
+ "Provincia de Zambezia": {
+ "lat": -16.5638987,
+ "lng": 36.6093926
+ },
+ "Manica Province": {
+ "lat": -18.1747162,
+ "lng": 33.438353
+ },
+ "Tete": {
+ "lat": -16.1328104,
+ "lng": 33.6063855
+ },
+ "Cabo Delgado Province": {
+ "lat": -12.3335474,
+ "lng": 39.3206241
+ },
+ "Nampula": {
+ "lat": -15.1266347,
+ "lng": 39.2687161
+ },
+ "Maputo Province": {
+ "lat": -25.593649,
+ "lng": 32.5372741
+ },
+ "Cidade de Maputo": {
+ "lat": -25.969248,
+ "lng": 32.5731746
+ },
+ "Sofala Province": {
+ "lat": -19.2039073,
+ "lng": 34.8624166
+ },
+ "": {
+ "lat": -18.665695,
+ "lng": 35.529562
+ }
+ },
+ "TH": {
+ "Prachuap Khiri Khan": {
+ "lat": 11.7938389,
+ "lng": 99.7957564
+ },
+ "Phetchaburi": {
+ "lat": 12.9035085,
+ "lng": 99.634135
+ },
+ "Surat Thani": {
+ "lat": 9.134194899999999,
+ "lng": 99.3334198
+ },
+ "Chiang Rai": {
+ "lat": 19.9104798,
+ "lng": 99.840576
+ },
+ "Trang": {
+ "lat": 7.564483299999999,
+ "lng": 99.6239334
+ },
+ "Nakhon Si Thammarat": {
+ "lat": 8.432483099999999,
+ "lng": 99.9599033
+ },
+ "Lampang": {
+ "lat": 18.2855395,
+ "lng": 99.5127895
+ },
+ "Chumphon": {
+ "lat": 10.5780131,
+ "lng": 99.1013498
+ },
+ "Phuket": {
+ "lat": 7.8804479,
+ "lng": 98.3922504
+ },
+ "Phang Nga": {
+ "lat": 8.6557757,
+ "lng": 98.3964938
+ },
+ "Tak": {
+ "lat": 16.9021534,
+ "lng": 99.0128926
+ },
+ "Sukhothai": {
+ "lat": 17.0076429,
+ "lng": 99.8264435
+ },
+ "Chiang Mai": {
+ "lat": 18.7883439,
+ "lng": 98.98530079999999
+ },
+ "Ratchaburi": {
+ "lat": 13.4509884,
+ "lng": 99.634135
+ },
+ "Ranong": {
+ "lat": 9.9658401,
+ "lng": 98.6408184
+ },
+ "Kamphaeng Phet": {
+ "lat": 16.4808481,
+ "lng": 99.5491144
+ },
+ "Phayao": {
+ "lat": 19.1709666,
+ "lng": 99.9067377
+ },
+ "Mae Hong Son": {
+ "lat": 18.7370307,
+ "lng": 97.87216
+ },
+ "Uthai Thani": {
+ "lat": 15.2998546,
+ "lng": 99.456155
+ },
+ "Lamphun": {
+ "lat": 18.1253143,
+ "lng": 98.92453429999999
+ },
+ "Phrae": {
+ "lat": 18.2509588,
+ "lng": 100.1703257
+ },
+ "Satun": {
+ "lat": 6.6120518,
+ "lng": 100.0723349
+ },
+ "Krabi": {
+ "lat": 8.0854803,
+ "lng": 98.9062856
+ },
+ "Kanchanaburi": {
+ "lat": 14.1011393,
+ "lng": 99.4179431
+ },
+ "Suphanburi": {
+ "lat": 14.5379162,
+ "lng": 99.99122539999999
+ },
+ "Nakhon Sawan": {
+ "lat": 15.6987382,
+ "lng": 100.11996
+ },
+ "Chai Nat": {
+ "lat": 15.1614193,
+ "lng": 100.1087187
+ },
+ "Nakhon Pathom": {
+ "lat": 13.8140293,
+ "lng": 100.0372929
+ },
+ "Samut Songkhram": {
+ "lat": 13.3931212,
+ "lng": 99.9465077
+ },
+ "Lopburi": {
+ "lat": 15.0499572,
+ "lng": 100.8903099
+ },
+ "Pathum Thani": {
+ "lat": 14.0033913,
+ "lng": 100.6647101
+ },
+ "Ubon Ratchathani": {
+ "lat": 15.2448453,
+ "lng": 104.8472995
+ },
+ "Nong Khai": {
+ "lat": 17.7696871,
+ "lng": 102.7594847
+ },
+ "Udon Thani": {
+ "lat": 17.3646969,
+ "lng": 102.8158924
+ },
+ "Nong Bua Lam Phu": {
+ "lat": 17.1739533,
+ "lng": 102.4495221
+ },
+ "Narathiwat": {
+ "lat": 6.190087999999999,
+ "lng": 101.7979613
+ },
+ "Yasothon": {
+ "lat": 15.795426,
+ "lng": 104.1418709
+ },
+ "Pattani": {
+ "lat": 6.869516,
+ "lng": 101.2504562
+ },
+ "Bangkok": {
+ "lat": 13.7563309,
+ "lng": 100.5017651
+ },
+ "Kalasin": {
+ "lat": 16.438508,
+ "lng": 103.5060994
+ },
+ "Yala": {
+ "lat": 6.5411018,
+ "lng": 101.2804075
+ },
+ "Saraburi": {
+ "lat": 14.5270426,
+ "lng": 100.9130244
+ },
+ "Phetchabun": {
+ "lat": 16.4243306,
+ "lng": 101.1617356
+ },
+ "Nan": {
+ "lat": 18.7838757,
+ "lng": 100.7789631
+ },
+ "Sa Kaeo": {
+ "lat": 13.8221849,
+ "lng": 102.0660435
+ },
+ "Phitsanulok": {
+ "lat": 16.8211238,
+ "lng": 100.2658516
+ },
+ "Phra Nakhon Si Ayutthaya": {
+ "lat": 14.3692325,
+ "lng": 100.5876634
+ },
+ "Si Sa Ket": {
+ "lat": 14.7579424,
+ "lng": 104.4723301
+ },
+ "Rayong": {
+ "lat": 12.6813957,
+ "lng": 101.2816261
+ },
+ "Uttaradit": {
+ "lat": 17.6698151,
+ "lng": 100.5296115
+ },
+ "Trat": {
+ "lat": 12.4202239,
+ "lng": 102.5298028
+ },
+ "Songkhla": {
+ "lat": 7.189765899999998,
+ "lng": 100.5953813
+ },
+ "Roi Et": {
+ "lat": 15.9032933,
+ "lng": 103.7289167
+ },
+ "Surin": {
+ "lat": 15.1696104,
+ "lng": 103.7289167
+ },
+ "Nakhon Phanom": {
+ "lat": 17.4115756,
+ "lng": 104.5655273
+ },
+ "Chanthaburi": {
+ "lat": 12.6112485,
+ "lng": 102.1037806
+ },
+ "Phichit": {
+ "lat": 16.1872019,
+ "lng": 100.3497895
+ },
+ "Nakhon Ratchasima": {
+ "lat": 14.9738493,
+ "lng": 102.083652
+ },
+ "Chon Buri": {
+ "lat": 13.2017461,
+ "lng": 101.2523792
+ },
+ "Sing Buri": {
+ "lat": 14.9148841,
+ "lng": 100.3273368
+ },
+ "Prachin Buri": {
+ "lat": 14.0965588,
+ "lng": 101.6157773
+ },
+ "Sakon Nakhon": {
+ "lat": 17.1664211,
+ "lng": 104.1486055
+ },
+ "Buriram": {
+ "lat": 14.9951003,
+ "lng": 103.1115915
+ },
+ "Chachoengsao": {
+ "lat": 13.5490952,
+ "lng": 101.6157773
+ },
+ "Samut Sakhon": {
+ "lat": 13.5497754,
+ "lng": 100.2740821
+ },
+ "Samut Prakan": {
+ "lat": 13.5954062,
+ "lng": 100.6072401
+ },
+ "Nonthaburi": {
+ "lat": 13.8591084,
+ "lng": 100.5216508
+ },
+ "Khon Kaen": {
+ "lat": 16.4321938,
+ "lng": 102.8236214
+ },
+ "Loei": {
+ "lat": 17.4220407,
+ "lng": 101.6157773
+ },
+ "Maha Sarakham": {
+ "lat": 16.1872403,
+ "lng": 103.3044979
+ },
+ "Phatthalung": {
+ "lat": 7.582847199999999,
+ "lng": 99.99122539999999
+ },
+ "Amnat Charoen": {
+ "lat": 15.9265678,
+ "lng": 104.7520939
+ },
+ "Bueng Kan": {
+ "lat": 18.326944,
+ "lng": 103.6883765
+ },
+ "Nakhon Nayok": {
+ "lat": 14.2038387,
+ "lng": 101.2268773
+ },
+ "Mukdahan": {
+ "lat": 16.5435914,
+ "lng": 104.7024121
+ },
+ "Chaiyaphum": {
+ "lat": 16.013875,
+ "lng": 101.8891721
+ },
+ "Ang Thong": {
+ "lat": 14.6583662,
+ "lng": 100.3947116
+ },
+ "": {
+ "lat": 15.870032,
+ "lng": 100.992541
+ }
+ },
+ "AF": {
+ "Helmand": {
+ "lat": 31.3636474,
+ "lng": 63.95861110000001
+ },
+ "Balkh": {
+ "lat": 36.7550603,
+ "lng": 66.8975372
+ },
+ "Kandahar": {
+ "lat": 31.628871,
+ "lng": 65.7371749
+ },
+ "Kabul": {
+ "lat": 34.5553494,
+ "lng": 69.207486
+ },
+ "Nangarhar": {
+ "lat": 34.1718313,
+ "lng": 70.6216794
+ },
+ "Herat": {
+ "lat": 34.352865,
+ "lng": 62.20402869999999
+ },
+ "Ghazni": {
+ "lat": 33.5450587,
+ "lng": 68.4173972
+ },
+ "Parwan": {
+ "lat": 34.9630977,
+ "lng": 68.81088489999999
+ },
+ "Baghlan": {
+ "lat": 36.17890260000001,
+ "lng": 68.74530639999999
+ },
+ "Khowst": {
+ "lat": 33.3338472,
+ "lng": 69.9371673
+ },
+ "": {
+ "lat": 33.93911,
+ "lng": 67.709953
+ }
+ },
+ "PK": {
+ "Sindh": {
+ "lat": 25.8943018,
+ "lng": 68.52471489999999
+ },
+ "Islamabad": {
+ "lat": 33.6844202,
+ "lng": 73.04788479999999
+ },
+ "Balochistan": {
+ "lat": 28.4907332,
+ "lng": 65.0957792
+ },
+ "Punjab": {
+ "lat": 31.1704063,
+ "lng": 72.7097161
+ },
+ "Khyber Pakhtunkhwa": {
+ "lat": 34.9526205,
+ "lng": 72.331113
+ },
+ "Azad Jammu and Kashmir": {
+ "lat": 33.9259055,
+ "lng": 73.7810334
+ },
+ "Gilgit-Baltistan": {
+ "lat": 35.80256670000001,
+ "lng": 74.9831808
+ },
+ "": {
+ "lat": 30.375321,
+ "lng": 69.345116
+ }
+ },
+ "BD": {
+ "Rajshahi Division": {
+ "lat": 24.7105776,
+ "lng": 88.94138650000001
+ },
+ "Rangpur Division": {
+ "lat": 25.8483388,
+ "lng": 88.94138650000001
+ },
+ "Dhaka Division": {
+ "lat": 23.9535742,
+ "lng": 90.14949879999999
+ },
+ "Chittagong": {
+ "lat": 22.3752075,
+ "lng": 91.8348606
+ },
+ "Sylhet Division": {
+ "lat": 24.7049811,
+ "lng": 91.67606909999999
+ },
+ "Mymensingh Division": {
+ "lat": 24.71362,
+ "lng": 90.4502368
+ },
+ "Khulna Division": {
+ "lat": 22.8087816,
+ "lng": 89.24671909999999
+ },
+ "Barisal Division": {
+ "lat": 22.3811131,
+ "lng": 90.3371889
+ },
+ "": {
+ "lat": 23.684994,
+ "lng": 90.356331
+ }
+ },
+ "ID": {
+ "North Sumatra": {
+ "lat": 2.1153547,
+ "lng": 99.54509739999999
+ },
+ "Aceh": {
+ "lat": 4.695135,
+ "lng": 96.7493993
+ },
+ "Yogyakarta": {
+ "lat": -7.7955798,
+ "lng": 110.3694896
+ },
+ "Central Java": {
+ "lat": -6.879704100000001,
+ "lng": 109.1255917
+ },
+ "East Java": {
+ "lat": -7.5360639,
+ "lng": 112.2384017
+ },
+ "South Sulawesi": {
+ "lat": -3.6687994,
+ "lng": 119.9740534
+ },
+ "Southeast Sulawesi": {
+ "lat": -4.144909999999999,
+ "lng": 122.174605
+ },
+ "West Java": {
+ "lat": -7.090910999999999,
+ "lng": 107.668887
+ },
+ "Bali": {
+ "lat": -8.4095178,
+ "lng": 115.188916
+ },
+ "North Sulawesi": {
+ "lat": 0.6246932,
+ "lng": 123.9750018
+ },
+ "Central Papua": {
+ "lat": -9.1360187,
+ "lng": 147.4627259
+ },
+ "North Maluku": {
+ "lat": 1.5709993,
+ "lng": 127.8087693
+ },
+ "Lampung": {
+ "lat": -4.5585849,
+ "lng": 105.4068079
+ },
+ "East Kalimantan": {
+ "lat": 0.5386586,
+ "lng": 116.419389
+ },
+ "Riau": {
+ "lat": 0.2933469,
+ "lng": 101.7068294
+ },
+ "Jakarta": {
+ "lat": -6.2087634,
+ "lng": 106.845599
+ },
+ "North Kalimantan": {
+ "lat": 3.0730929,
+ "lng": 116.0413889
+ },
+ "Riau Islands": {
+ "lat": 3.9456514,
+ "lng": 108.1428669
+ },
+ "South Kalimantan": {
+ "lat": -3.0926415,
+ "lng": 115.2837585
+ },
+ "Banten": {
+ "lat": -6.4058172,
+ "lng": 106.0640179
+ },
+ "Jambi": {
+ "lat": -1.6101229,
+ "lng": 103.6131203
+ },
+ "Bangka–Belitung Islands": {
+ "lat": -2.7410513,
+ "lng": 106.4405872
+ },
+ "West Nusa Tenggara": {
+ "lat": -8.6529334,
+ "lng": 117.3616476
+ },
+ "South Sumatra": {
+ "lat": -3.3194374,
+ "lng": 103.914399
+ },
+ "West Sumatra": {
+ "lat": -0.7399397,
+ "lng": 100.8000051
+ },
+ "South Papua": {
+ "lat": -4.269928,
+ "lng": 138.0803529
+ },
+ "West Kalimantan": {
+ "lat": -0.2787808,
+ "lng": 111.4752851
+ },
+ "Central Kalimantan": {
+ "lat": -1.6814878,
+ "lng": 113.3823545
+ },
+ "West Sulawesi": {
+ "lat": -2.8441371,
+ "lng": 119.2320784
+ },
+ "Maluku": {
+ "lat": -3.2384616,
+ "lng": 130.1452734
+ },
+ "Central Sulawesi": {
+ "lat": -1.4300254,
+ "lng": 121.4456179
+ },
+ "West Papua": {
+ "lat": -1.3361154,
+ "lng": 133.1747162
+ },
+ "East Nusa Tenggara": {
+ "lat": -8.657381899999999,
+ "lng": 121.0793705
+ },
+ "Papua": {
+ "lat": -4.269928,
+ "lng": 138.0803529
+ },
+ "Gorontalo": {
+ "lat": 0.5435441999999999,
+ "lng": 123.0567693
+ },
+ "Bengkulu": {
+ "lat": -3.792845099999999,
+ "lng": 102.2607641
+ },
+ "": {
+ "lat": -0.789275,
+ "lng": 113.921327
+ }
+ },
+ "TJ": {
+ "Viloyati Khatlon": {
+ "lat": 37.9113562,
+ "lng": 69.097023
+ },
+ "Viloyati Sughd": {
+ "lat": 39.5155326,
+ "lng": 69.097023
+ },
+ "Republican Subordination": {
+ "lat": 39.0857902,
+ "lng": 70.2408325
+ },
+ "Gorno-Badakhshan": {
+ "lat": 38.412732,
+ "lng": 73.087749
+ },
+ "Dushanbe": {
+ "lat": 38.5597722,
+ "lng": 68.7870384
+ },
+ "": {
+ "lat": 38.861034,
+ "lng": 71.276093
+ }
+ },
+ "MY": {
+ "Kedah": {
+ "lat": 6.0498656,
+ "lng": 100.5296115
+ },
+ "Penang": {
+ "lat": 5.414130699999999,
+ "lng": 100.3287506
+ },
+ "Pahang": {
+ "lat": 3.9743406,
+ "lng": 102.4380581
+ },
+ "Perak": {
+ "lat": 4.807294,
+ "lng": 100.8000051
+ },
+ "Selangor": {
+ "lat": 3.509247,
+ "lng": 101.5248055
+ },
+ "Johor": {
+ "lat": 1.9343998,
+ "lng": 103.3587288
+ },
+ "Terengganu": {
+ "lat": 5.093634199999999,
+ "lng": 102.989615
+ },
+ "Sarawak": {
+ "lat": 2.5574285,
+ "lng": 113.0011989
+ },
+ "Sabah": {
+ "lat": 5.4204043,
+ "lng": 116.7967849
+ },
+ "Kelantan": {
+ "lat": 5.115146,
+ "lng": 101.8891721
+ },
+ "Kuala Lumpur": {
+ "lat": 3.1319197,
+ "lng": 101.6840589
+ },
+ "Labuan": {
+ "lat": 5.2831456,
+ "lng": 115.230825
+ },
+ "Melaka": {
+ "lat": 2.189594,
+ "lng": 102.2500868
+ },
+ "Negeri Sembilan": {
+ "lat": 2.8707497,
+ "lng": 102.2547919
+ },
+ "Perlis": {
+ "lat": 6.5170189,
+ "lng": 100.2151578
+ },
+ "Putrajaya": {
+ "lat": 2.926361,
+ "lng": 101.696445
+ },
+ "": {
+ "lat": 4.210484,
+ "lng": 101.975766
+ }
+ },
+ "LK": {
+ "Western Province": {
+ "lat": 6.901608599999999,
+ "lng": 80.0087746
+ },
+ "Southern Province": {
+ "lat": 6.237375,
+ "lng": 80.54384499999999
+ },
+ "North Western Province": {
+ "lat": 7.758409100000001,
+ "lng": 80.1875065
+ },
+ "Province of Uva": {
+ "lat": 6.8427612,
+ "lng": 81.3399414
+ },
+ "Sabaragamuwa Province": {
+ "lat": 6.7395941,
+ "lng": 80.365865
+ },
+ "Northern Province": {
+ "lat": 8.8855027,
+ "lng": 80.2767327
+ },
+ "Central Province": {
+ "lat": 7.256499600000001,
+ "lng": 80.7214417
+ },
+ "North Central Province": {
+ "lat": 8.1995638,
+ "lng": 80.6326916
+ },
+ "Eastern Province": {
+ "lat": 7.7853051,
+ "lng": 81.42789839999999
+ },
+ "": {
+ "lat": 7.873054,
+ "lng": 80.771797
+ }
+ },
+ "BT": {
+ "Trongsa Dzongkhag": {
+ "lat": 27.2666292,
+ "lng": 90.52578229999999
+ },
+ "Thimphu District": {
+ "lat": 27.471586,
+ "lng": 89.6386108
+ },
+ "Trashi Yangste": {
+ "lat": 27.8236356,
+ "lng": 91.44346900000001
+ },
+ "Trashigang District": {
+ "lat": 27.2566795,
+ "lng": 91.7538817
+ },
+ "Dagana": {
+ "lat": 27.0322861,
+ "lng": 89.8879304
+ },
+ "Zhemgang District": {
+ "lat": 27.076975,
+ "lng": 90.8294002
+ },
+ "Sarpang District": {
+ "lat": 26.9373041,
+ "lng": 90.4879916
+ },
+ "Punakha Dzongkhag": {
+ "lat": 27.5920869,
+ "lng": 89.87974589999999
+ },
+ "Paro": {
+ "lat": 27.428684,
+ "lng": 89.41636539999999
+ },
+ "Mongar": {
+ "lat": 27.2753739,
+ "lng": 91.23977750000002
+ },
+ "Lhuntse": {
+ "lat": 27.6649208,
+ "lng": 91.1761004
+ },
+ "Haa": {
+ "lat": 27.386227,
+ "lng": 89.27760169999999
+ },
+ "Gasa": {
+ "lat": 27.8982746,
+ "lng": 89.7309534
+ },
+ "Chukha": {
+ "lat": 27.0522919,
+ "lng": 89.5756987
+ },
+ "Bumthang District": {
+ "lat": 27.641839,
+ "lng": 90.6773046
+ },
+ "Samtse District": {
+ "lat": 27.0291832,
+ "lng": 89.0561532
+ },
+ "Wangdue Phodrang Dzongkhag": {
+ "lat": 27.4526046,
+ "lng": 90.0674928
+ },
+ "Samdrup Jongkhar": {
+ "lat": 26.8035682,
+ "lng": 91.5039207
+ },
+ "": {
+ "lat": 27.514162,
+ "lng": 90.433601
+ }
+ },
+ "IN": {
+ "Maharashtra": {
+ "lat": 19.7514798,
+ "lng": 75.7138884
+ },
+ "Karnataka": {
+ "lat": 15.3172775,
+ "lng": 75.7138884
+ },
+ "Telangana": {
+ "lat": 18.1124372,
+ "lng": 79.01929969999999
+ },
+ "Haryana": {
+ "lat": 29.0587757,
+ "lng": 76.085601
+ },
+ "Nagaland": {
+ "lat": 26.1584354,
+ "lng": 94.5624426
+ },
+ "West Bengal": {
+ "lat": 22.9867569,
+ "lng": 87.8549755
+ },
+ "Tamil Nadu": {
+ "lat": 11.1271225,
+ "lng": 78.6568942
+ },
+ "Gujarat": {
+ "lat": 22.6708317,
+ "lng": 71.5723953
+ },
+ "Kerala": {
+ "lat": 10.1631526,
+ "lng": 76.64127119999999
+ },
+ "Andhra Pradesh": {
+ "lat": 15.9128998,
+ "lng": 79.7399875
+ },
+ "Manipur": {
+ "lat": 24.6637173,
+ "lng": 93.90626879999999
+ },
+ "Madhya Pradesh": {
+ "lat": 22.9734229,
+ "lng": 78.6568942
+ },
+ "Goa": {
+ "lat": 15.2993265,
+ "lng": 74.12399599999999
+ },
+ "Uttar Pradesh": {
+ "lat": 27.5705886,
+ "lng": 80.0981869
+ },
+ "Odisha": {
+ "lat": 20.2375561,
+ "lng": 84.2700179
+ },
+ "Uttarakhand": {
+ "lat": 30.066753,
+ "lng": 79.01929969999999
+ },
+ "Chhattisgarh": {
+ "lat": 21.2786567,
+ "lng": 81.8661442
+ },
+ "Himachal Pradesh": {
+ "lat": 32.1024076,
+ "lng": 77.5619419
+ },
+ "Assam": {
+ "lat": 26.2006043,
+ "lng": 92.9375739
+ },
+ "Rajasthan": {
+ "lat": 27.0238036,
+ "lng": 74.21793260000001
+ },
+ "Tripura": {
+ "lat": 23.5638643,
+ "lng": 91.67606909999999
+ },
+ "Meghalaya": {
+ "lat": 25.4670308,
+ "lng": 91.366216
+ },
+ "Jharkhand": {
+ "lat": 23.6913486,
+ "lng": 85.2722472
+ },
+ "Punjab": {
+ "lat": 31.1471305,
+ "lng": 75.34121789999999
+ },
+ "Bihar": {
+ "lat": 25.9644427,
+ "lng": 85.2722472
+ },
+ "Jammu and Kashmir": {
+ "lat": 33.277839,
+ "lng": 75.34121789999999
+ },
+ "Sikkim": {
+ "lat": 27.3516407,
+ "lng": 88.32393090000001
+ },
+ "Dadra and Nagar Haveli and Daman and Diu": {
+ "lat": 20.1808672,
+ "lng": 73.0169135
+ },
+ "Andaman and Nicobar": {
+ "lat": 10.7448873,
+ "lng": 92.49999179999999
+ },
+ "Mizoram": {
+ "lat": 23.164543,
+ "lng": 92.9375739
+ },
+ "Union Territory of Puducherry": {
+ "lat": 11.941685,
+ "lng": 79.8083177
+ },
+ "National Capital Territory of Delhi": {
+ "lat": 28.7040592,
+ "lng": 77.10249019999999
+ },
+ "Ladakh": {
+ "lat": 34.2268475,
+ "lng": 77.5619419
+ },
+ "Lakshadweep": {
+ "lat": 9.985089192440064,
+ "lng": 72.93247324992308
+ },
+ "Arunachal Pradesh": {
+ "lat": 28.2179994,
+ "lng": 94.7277528
+ },
+ "Chandigarh": {
+ "lat": 30.7333148,
+ "lng": 76.7794179
+ },
+ "": {
+ "lat": 20.593684,
+ "lng": 78.96288
+ }
+ },
+ "CN": {
+ "Tibet": {
+ "lat": 29.6472399,
+ "lng": 91.11744999999999
+ },
+ "Gansu": {
+ "lat": 36.0594199,
+ "lng": 103.82634
+ },
+ "Xinjiang Uyghur Autonomous Region": {
+ "lat": 43.7934299,
+ "lng": 87.6271
+ },
+ "Qinghai": {
+ "lat": 36.6208699,
+ "lng": 101.7801099
+ },
+ "Yunnan": {
+ "lat": 25.0452999,
+ "lng": 102.7097299
+ },
+ "Guizhou": {
+ "lat": 26.5981999,
+ "lng": 106.70722
+ },
+ "Anhui": {
+ "lat": 31.8615699,
+ "lng": 117.28565
+ },
+ "Shandong": {
+ "lat": 36.6682599,
+ "lng": 117.02076
+ },
+ "Sichuan": {
+ "lat": 30.6508899,
+ "lng": 104.07572
+ },
+ "Hunan": {
+ "lat": 28.1142216,
+ "lng": 112.9833341
+ },
+ "Henan": {
+ "lat": 34.7657099,
+ "lng": 113.75322
+ },
+ "Zhejiang": {
+ "lat": 30.2655499,
+ "lng": 120.1536
+ },
+ "Liaoning": {
+ "lat": 41.8357099,
+ "lng": 123.42925
+ },
+ "Ningxia Hui Autonomous Region": {
+ "lat": 38.4711699,
+ "lng": 106.25867
+ },
+ "Jiangsu": {
+ "lat": 32.0607099,
+ "lng": 118.76295
+ },
+ "Hebei": {
+ "lat": 38.0359899,
+ "lng": 114.46979
+ },
+ "Shanxi": {
+ "lat": 37.8734299,
+ "lng": 112.56272
+ },
+ "Guangdong": {
+ "lat": 23.1317099,
+ "lng": 113.26627
+ },
+ "Fujian": {
+ "lat": 26.0998199,
+ "lng": 119.2965899
+ },
+ "Hubei": {
+ "lat": 30.5453899,
+ "lng": 114.34234
+ },
+ "Guangxi": {
+ "lat": 24.3255257,
+ "lng": 109.4154583
+ },
+ "Jiangxi": {
+ "lat": 28.6741699,
+ "lng": 115.91004
+ },
+ "Hainan": {
+ "lat": 20.0199699,
+ "lng": 110.34863
+ },
+ "Tianjin": {
+ "lat": 39.0850999,
+ "lng": 117.19937
+ },
+ "Shaanxi": {
+ "lat": 34.2648599,
+ "lng": 108.95424
+ },
+ "Shanghai": {
+ "lat": 31.230416,
+ "lng": 121.473701
+ },
+ "Chongqing": {
+ "lat": 29.5656843,
+ "lng": 106.5511838
+ },
+ "Beijing": {
+ "lat": 39.904211,
+ "lng": 116.407395
+ },
+ "Heilongjiang": {
+ "lat": 45.7420799,
+ "lng": 126.66285
+ },
+ "Jilin": {
+ "lat": 43.8378399,
+ "lng": 126.54944
+ },
+ "Inner Mongolia Autonomous Region": {
+ "lat": 40.8173299,
+ "lng": 111.76522
+ },
+ "": {
+ "lat": 35.86166,
+ "lng": 104.195397
+ }
+ },
+ "MV": {
+ "Addu Atoll": {
+ "lat": -0.6300994999999999,
+ "lng": 73.1585626
+ },
+ "Kaafu Atoll": {
+ "lat": 4.4558979,
+ "lng": 73.55941279999999
+ },
+ "Haa Dhaalu Atholhu": {
+ "lat": 6.578271699999999,
+ "lng": 72.94605659999999
+ },
+ "Southern Ari Atoll": {
+ "lat": 3.6543302,
+ "lng": 72.8042797
+ },
+ "Baa Atholhu": {
+ "lat": 5.2396042,
+ "lng": 72.99329689999999
+ },
+ "Dhaalu Atholhu": {
+ "lat": 2.8468502,
+ "lng": 72.94605659999999
+ },
+ "Faafu Atholhu": {
+ "lat": 3.2309409,
+ "lng": 72.94605659999999
+ },
+ "Gnyaviyani Atoll": {
+ "lat": -0.3006425,
+ "lng": 73.42391429999999
+ },
+ "Haa Alifu Atholhu": {
+ "lat": 6.9903488,
+ "lng": 72.94605659999999
+ },
+ "Faadhippolhu Atoll": {
+ "lat": 5.3747021,
+ "lng": 73.5122928
+ },
+ "Meemu Atholhu": {
+ "lat": 3.0090345,
+ "lng": 73.5122928
+ },
+ "Shaviyani Atholhu": {
+ "lat": 6.17511,
+ "lng": 73.13496049999999
+ },
+ "Thaa Atholhu": {
+ "lat": 2.4311161,
+ "lng": 73.1821623
+ },
+ "Vaavu Atholhu": {
+ "lat": 3.3955438,
+ "lng": 73.5122928
+ },
+ "Male": {
+ "lat": 4.1754959,
+ "lng": 73.5093474
+ },
+ "Laamu Atholhu": {
+ "lat": 1.9430737,
+ "lng": 73.4180211
+ },
+ "Northern Ari Atoll": {
+ "lat": 4.085,
+ "lng": 72.8515479
+ },
+ "": {
+ "lat": 3.202778,
+ "lng": 73.22068
+ }
+ },
+ "NP": {
+ "Province 1": {
+ "lat": 27.3372148,
+ "lng": 87.38107269999999
+ },
+ "Sudurpashchim Pradesh": {
+ "lat": 29.2987871,
+ "lng": 80.9871074
+ },
+ "Lumbini Province": {
+ "lat": 27.9207402,
+ "lng": 82.7347142
+ },
+ "Gandaki Pradesh": {
+ "lat": 28.3590669,
+ "lng": 84.1012861
+ },
+ "Karnali Pradesh": {
+ "lat": 29.3862555,
+ "lng": 82.38857829999999
+ },
+ "Bagmati Province": {
+ "lat": 27.6624958,
+ "lng": 85.4375574
+ },
+ "Province 2": {
+ "lat": 27.0135298,
+ "lng": 85.684578
+ },
+ "": {
+ "lat": 28.394857,
+ "lng": 84.124008
+ }
+ },
+ "MM": {
+ "Magway Region": {
+ "lat": 19.8871386,
+ "lng": 94.7277528
+ },
+ "Bago Region": {
+ "lat": 18.3312802,
+ "lng": 96.0679194
+ },
+ "Tanintharyi Region": {
+ "lat": 12.4706876,
+ "lng": 99.0128926
+ },
+ "Shan State": {
+ "lat": 22.0361985,
+ "lng": 98.1338558
+ },
+ "Yangon": {
+ "lat": 16.840939,
+ "lng": 96.173526
+ },
+ "Mandalay Region": {
+ "lat": 21.5619058,
+ "lng": 95.89871389999999
+ },
+ "Kachin State": {
+ "lat": 25.850904,
+ "lng": 97.4381355
+ },
+ "Mon State": {
+ "lat": 16.3003133,
+ "lng": 97.69822719999999
+ },
+ "Sagaing Region": {
+ "lat": 24.428381,
+ "lng": 95.3939551
+ },
+ "Kayah State": {
+ "lat": 19.2342061,
+ "lng": 97.26528580000002
+ },
+ "Kayin State": {
+ "lat": 16.9459346,
+ "lng": 97.9592863
+ },
+ "Chin State": {
+ "lat": 22.0086978,
+ "lng": 93.5812692
+ },
+ "Ayeyarwady Region": {
+ "lat": 17.0342125,
+ "lng": 95.22666749999999
+ },
+ "Nay Pyi Taw": {
+ "lat": 19.7633057,
+ "lng": 96.07851040000001
+ },
+ "": {
+ "lat": 21.913965,
+ "lng": 95.956223
+ }
+ },
+ "MN": {
+ "Dzavhan Aymag": {
+ "lat": 48.2388147,
+ "lng": 96.07030189999999
+ },
+ "Uvs Province": {
+ "lat": 49.64497069999999,
+ "lng": 93.27365759999999
+ },
+ "Govi-Altai Province": {
+ "lat": 45.4511227,
+ "lng": 95.8505766
+ },
+ "Bayan-OElgiy Aymag": {
+ "lat": 48.3983254,
+ "lng": 89.66259149999999
+ },
+ "Hovd": {
+ "lat": 47.9795218,
+ "lng": 91.634756
+ },
+ "Ulaanbaatar Hot": {
+ "lat": 47.88639879999999,
+ "lng": 106.9057439
+ },
+ "Bulgan": {
+ "lat": 48.8231572,
+ "lng": 103.5218199
+ },
+ "Arkhangai Province": {
+ "lat": 47.8971101,
+ "lng": 100.7240165
+ },
+ "Middle Govĭ": {
+ "lat": 45.5822786,
+ "lng": 106.7644209
+ },
+ "Ömnögovĭ": {
+ "lat": 43.500024,
+ "lng": 104.2861116
+ },
+ "Suehbaatar Aymag": {
+ "lat": 46.5653163,
+ "lng": 113.5380836
+ },
+ "Hentiy Aymag": {
+ "lat": 47.6081209,
+ "lng": 109.9372856
+ },
+ "East Gobi Aymag": {
+ "lat": 43.9653889,
+ "lng": 109.1773459
+ },
+ "Khövsgöl Province": {
+ "lat": 50.2204484,
+ "lng": 100.3213768
+ },
+ "Orhon Aymag": {
+ "lat": 49.0366226,
+ "lng": 104.3749853
+ },
+ "South Khangay": {
+ "lat": 45.84891589999999,
+ "lng": 102.3244613
+ },
+ "Central Aymag": {
+ "lat": 47.2124056,
+ "lng": 106.41541
+ },
+ "Govi-Sumber": {
+ "lat": 46.4762754,
+ "lng": 108.5570627
+ },
+ "East Aimak": {
+ "lat": 48.2227684,
+ "lng": 115.0389646
+ },
+ "Bayanhongor Aymag": {
+ "lat": 46.177215,
+ "lng": 100.7118653
+ },
+ "Selenge Aymag": {
+ "lat": 49.0183549,
+ "lng": 106.3334287
+ },
+ "": {
+ "lat": 46.862496,
+ "lng": 103.846656
+ }
+ },
+ "KG": {
+ "Issyk-Kul": {
+ "lat": 42.3882639,
+ "lng": 77.2864879
+ },
+ "Chuyskaya Oblast'": {
+ "lat": 42.5655,
+ "lng": 74.4056612
+ },
+ "Jalal-Abad oblast": {
+ "lat": 41.106808,
+ "lng": 72.8988069
+ },
+ "Talas": {
+ "lat": 42.5317628,
+ "lng": 72.2304571
+ },
+ "Osh Oblasty": {
+ "lat": 39.8407366,
+ "lng": 72.8988069
+ },
+ "Batken": {
+ "lat": 40.054846,
+ "lng": 70.82091439999999
+ },
+ "Gorod Bishkek": {
+ "lat": 42.8746212,
+ "lng": 74.5697617
+ },
+ "": {
+ "lat": 41.20438,
+ "lng": 74.766098
+ }
+ },
+ "PW": {
+ "State of Koror": {
+ "lat": 7.3375646,
+ "lng": 134.4889469
+ },
+ "State of Aimeliik": {
+ "lat": 7.445585899999998,
+ "lng": 134.5030878
+ },
+ "State of Airai": {
+ "lat": 7.396611799999999,
+ "lng": 134.5690225
+ },
+ "State of Angaur": {
+ "lat": 6.909223,
+ "lng": 134.1387934
+ },
+ "State of Melekeok": {
+ "lat": 7.515028599999999,
+ "lng": 134.5972518
+ },
+ "State of Ngeremlengui": {
+ "lat": 7.5198397,
+ "lng": 134.5596089
+ },
+ "State of Ngatpang": {
+ "lat": 7.4710994,
+ "lng": 134.5266466
+ },
+ "State of Ngarchelong": {
+ "lat": 7.7105469,
+ "lng": 134.6301646
+ },
+ "State of Sonsorol": {
+ "lat": 5.3268119,
+ "lng": 132.2239117
+ },
+ "State of Kayangel": {
+ "lat": 8.07,
+ "lng": 134.702778
+ },
+ "State of Peleliu": {
+ "lat": 7.002290599999999,
+ "lng": 134.2431628
+ },
+ "State of Ngardmau": {
+ "lat": 7.585048599999999,
+ "lng": 134.5596089
+ },
+ "": {
+ "lat": 7.51498,
+ "lng": 134.58252
+ }
+ },
+ "VN": {
+ "Tinh Nghe An": {
+ "lat": 19.2342489,
+ "lng": 104.9200365
+ },
+ "Tinh Soc Trang": {
+ "lat": 9.6003688,
+ "lng": 105.9599539
+ },
+ "Tinh Tra Vinh": {
+ "lat": 9.812740999999999,
+ "lng": 106.2992912
+ },
+ "Tinh Vinh Long": {
+ "lat": 10.0861281,
+ "lng": 106.0169971
+ },
+ "Hanoi": {
+ "lat": 21.0277644,
+ "lng": 105.8341598
+ },
+ "Tinh Ninh Binh": {
+ "lat": 20.2129969,
+ "lng": 105.92299
+ },
+ "Tinh Yen Bai": {
+ "lat": 21.6837923,
+ "lng": 104.4551361
+ },
+ "Tinh GJong Nai": {
+ "lat": 11.0686305,
+ "lng": 107.1675976
+ },
+ "Ho Chi Minh": {
+ "lat": 10.8230989,
+ "lng": 106.6296638
+ },
+ "Tinh Ba Ria-Vung Tau": {
+ "lat": 10.5417397,
+ "lng": 107.2429976
+ },
+ "Tinh Phu Tho": {
+ "lat": 21.268443,
+ "lng": 105.2045573
+ },
+ "Hậu Giang": {
+ "lat": 9.757897999999999,
+ "lng": 105.6412527
+ },
+ "Tinh Vinh Phuc": {
+ "lat": 21.3608805,
+ "lng": 105.5474373
+ },
+ "Tinh Binh GJinh": {
+ "lat": 15.193231,
+ "lng": 108.7185184
+ },
+ "Tinh Khanh Hoa": {
+ "lat": 12.2585098,
+ "lng": 109.0526076
+ },
+ "Tinh Quang Ninh": {
+ "lat": 21.006382,
+ "lng": 107.2925144
+ },
+ "Tinh Phu Yen": {
+ "lat": 13.0881861,
+ "lng": 109.0928764
+ },
+ "Tinh Tuyen Quang": {
+ "lat": 22.1726708,
+ "lng": 105.3131185
+ },
+ "Tiền Giang": {
+ "lat": 10.4493324,
+ "lng": 106.3420504
+ },
+ "Haiphong": {
+ "lat": 20.8449115,
+ "lng": 106.6880841
+ },
+ "Tinh Binh Duong": {
+ "lat": 11.3254024,
+ "lng": 106.477017
+ },
+ "Tinh Quang Nam": {
+ "lat": 15.5393538,
+ "lng": 108.019102
+ },
+ "Tinh Thanh Hoa": {
+ "lat": 20.1291279,
+ "lng": 105.3131185
+ },
+ "Can Tho": {
+ "lat": 10.0451618,
+ "lng": 105.7468535
+ },
+ "Tinh Lai Chau": {
+ "lat": 22.3686613,
+ "lng": 103.3119085
+ },
+ "Tinh Thai Nguyen": {
+ "lat": 21.5613771,
+ "lng": 105.876004
+ },
+ "Tinh Thai Binh": {
+ "lat": 20.5386936,
+ "lng": 106.3934777
+ },
+ "Tây Ninh Province": {
+ "lat": 11.3494766,
+ "lng": 106.0640179
+ },
+ "Long An": {
+ "lat": 10.5607168,
+ "lng": 106.6497623
+ },
+ "An Giang": {
+ "lat": 10.5215836,
+ "lng": 105.1258955
+ },
+ "Tinh Son La": {
+ "lat": 21.1022284,
+ "lng": 103.7289167
+ },
+ "Tinh Lao Cai": {
+ "lat": 22.3380865,
+ "lng": 104.1487055
+ },
+ "Tinh GJong Thap": {
+ "lat": 14.058324,
+ "lng": 108.277199
+ },
+ "Tinh Kien Giang": {
+ "lat": 9.8249587,
+ "lng": 105.1258955
+ },
+ "Tinh Quang Tri": {
+ "lat": 16.7943472,
+ "lng": 106.963409
+ },
+ "Tinh Quang Binh": {
+ "lat": 17.6102715,
+ "lng": 106.3487474
+ },
+ "Quảng Ngãi Province": {
+ "lat": 15.0759838,
+ "lng": 108.7125791
+ },
+ "Gia Lai": {
+ "lat": 13.8078943,
+ "lng": 108.109375
+ },
+ "Tinh Bac Ninh": {
+ "lat": 21.121444,
+ "lng": 106.1110501
+ },
+ "Tinh Hoa Binh": {
+ "lat": 20.6861265,
+ "lng": 105.3131185
+ },
+ "Tinh Ha Nam": {
+ "lat": 20.5835196,
+ "lng": 105.92299
+ },
+ "Tinh Thua Thien-Hue": {
+ "lat": 16.467397,
+ "lng": 107.5905326
+ },
+ "Tinh Binh Thuan": {
+ "lat": 11.0903703,
+ "lng": 108.0720781
+ },
+ "Tinh Ninh Thuan": {
+ "lat": 11.6738767,
+ "lng": 108.8629572
+ },
+ "Tinh Nam GJinh": {
+ "lat": 14.058324,
+ "lng": 108.277199
+ },
+ "Tinh Bac Giang": {
+ "lat": 21.3014947,
+ "lng": 106.6291304
+ },
+ "Tinh Lang Son": {
+ "lat": 21.8563705,
+ "lng": 106.6291304
+ },
+ "Tinh Lam GJong": {
+ "lat": 14.058324,
+ "lng": 108.277199
+ },
+ "Tinh Dien Bien": {
+ "lat": 21.8042309,
+ "lng": 103.1076525
+ },
+ "Kon Tum": {
+ "lat": 14.3497403,
+ "lng": 108.0004606
+ },
+ "Tinh Hung Yen": {
+ "lat": 20.8525711,
+ "lng": 106.0169971
+ },
+ "Tinh Ha Tinh": {
+ "lat": 18.2943776,
+ "lng": 105.6745247
+ },
+ "Tinh Hai Duong": {
+ "lat": 20.9385958,
+ "lng": 106.3206861
+ },
+ "Tinh Ha Giang": {
+ "lat": 22.7662056,
+ "lng": 104.9388853
+ },
+ "Dak Nong": {
+ "lat": 12.2646476,
+ "lng": 107.609806
+ },
+ "Tinh Binh Phuoc": {
+ "lat": 11.7511894,
+ "lng": 106.7234639
+ },
+ "Da Nang": {
+ "lat": 16.0544563,
+ "lng": 108.0717219
+ },
+ "Đắk Lắk": {
+ "lat": 12.7100116,
+ "lng": 108.2377519
+ },
+ "Tinh Ben Tre": {
+ "lat": 10.1081553,
+ "lng": 106.4405872
+ },
+ "Tinh Bac Lieu": {
+ "lat": 9.251555500000002,
+ "lng": 105.5136472
+ },
+ "Tinh Cao Bang": {
+ "lat": 22.635689,
+ "lng": 106.2522143
+ },
+ "Tinh Ca Mau": {
+ "lat": 8.962409899999999,
+ "lng": 105.1258955
+ },
+ "Tinh Bac Kan": {
+ "lat": 22.3032923,
+ "lng": 105.876004
+ },
+ "": {
+ "lat": 14.058324,
+ "lng": 108.277199
+ }
+ },
+ "TL": {
+ "Viqueque": {
+ "lat": -8.8597918,
+ "lng": 126.3633516
+ },
+ "Cova Lima": {
+ "lat": -9.2650375,
+ "lng": 125.2587964
+ },
+ "Manufahi": {
+ "lat": -9.0145495,
+ "lng": 125.8279959
+ },
+ "Oecusse": {
+ "lat": -9.2970576,
+ "lng": 124.2866091
+ },
+ "Manatuto": {
+ "lat": -8.5155608,
+ "lng": 126.0159255
+ },
+ "Bobonaro": {
+ "lat": -9.0320375,
+ "lng": 125.3233049
+ },
+ "Liquiçá": {
+ "lat": -8.5891214,
+ "lng": 125.3411388
+ },
+ "Ainaro": {
+ "lat": -8.996518199999999,
+ "lng": 125.5083136
+ },
+ "Dili": {
+ "lat": -8.5568557,
+ "lng": 125.5603143
+ },
+ "Baucau": {
+ "lat": -8.4731006,
+ "lng": 126.4553727
+ },
+ "Aileu": {
+ "lat": -8.722408399999999,
+ "lng": 125.5712576
+ },
+ "Lautém": {
+ "lat": -8.5531846,
+ "lng": 126.9970831
+ },
+ "Ermera": {
+ "lat": -8.752480199999999,
+ "lng": 125.3987294
+ },
+ "": {
+ "lat": -8.874217,
+ "lng": 125.727539
+ }
+ },
+ "LA": {
+ "Vientiane Prefecture": {
+ "lat": 18.110541,
+ "lng": 102.5298028
+ },
+ "Houaphan": {
+ "lat": 20.3254175,
+ "lng": 104.1001326
+ },
+ "Khoueng Savannakhet": {
+ "lat": 16.5065381,
+ "lng": 105.5943388
+ },
+ "Salavan": {
+ "lat": 15.7165658,
+ "lng": 106.4140868
+ },
+ "Champasak": {
+ "lat": 14.9030362,
+ "lng": 105.8642593
+ },
+ "Xaignabouli": {
+ "lat": 19.1945799,
+ "lng": 101.483498
+ },
+ "Vientiane Province": {
+ "lat": 18.9686447,
+ "lng": 102.2547919
+ },
+ "Bolikhamsai": {
+ "lat": 18.4362924,
+ "lng": 104.4723301
+ },
+ "Khammouan": {
+ "lat": 17.6384066,
+ "lng": 105.2194808
+ },
+ "Louangnamtha": {
+ "lat": 20.9709135,
+ "lng": 101.4112049
+ },
+ "Xiangkhouang": {
+ "lat": 19.6375088,
+ "lng": 103.3587288
+ },
+ "Xaisomboun": {
+ "lat": 18.9181183,
+ "lng": 102.989615
+ },
+ "Khoueng Phongsali": {
+ "lat": 21.5919377,
+ "lng": 102.2547919
+ },
+ "Khoueng Oudomxai": {
+ "lat": 20.4921929,
+ "lng": 101.8891721
+ },
+ "Attapu": {
+ "lat": 14.8193696,
+ "lng": 106.8207875
+ },
+ "Khoueng Xekong": {
+ "lat": 15.5767446,
+ "lng": 107.0067031
+ },
+ "": {
+ "lat": 19.85627,
+ "lng": 102.495496
+ }
+ },
+ "TW": {
+ "New Taipei": {
+ "lat": 25.0329694,
+ "lng": 121.5654177
+ },
+ "Yunlin": {
+ "lat": 23.7092033,
+ "lng": 120.4313373
+ },
+ "Tainan": {
+ "lat": 22.9997281,
+ "lng": 120.2270277
+ },
+ "Changhua": {
+ "lat": 24.0716583,
+ "lng": 120.5624474
+ },
+ "Nantou": {
+ "lat": 23.9179593,
+ "lng": 120.6775164
+ },
+ "Yilan": {
+ "lat": 24.7021073,
+ "lng": 121.7377502
+ },
+ "Kaohsiung": {
+ "lat": 22.6272784,
+ "lng": 120.3014353
+ },
+ "Taoyuan": {
+ "lat": 24.9936281,
+ "lng": 121.3009798
+ },
+ "Taichung City": {
+ "lat": 24.1477358,
+ "lng": 120.6736482
+ },
+ "Hualien": {
+ "lat": 23.9871589,
+ "lng": 121.6015714
+ },
+ "Pingtung": {
+ "lat": 22.6710655,
+ "lng": 120.4892658
+ },
+ "Miaoli": {
+ "lat": 24.560159,
+ "lng": 120.8214265
+ },
+ "Chiayi County": {
+ "lat": 23.4518428,
+ "lng": 120.2554615
+ },
+ "Hsinchu County": {
+ "lat": 24.8387226,
+ "lng": 121.0177246
+ },
+ "Taitung": {
+ "lat": 22.7613207,
+ "lng": 121.1438152
+ },
+ "Taipei City": {
+ "lat": 25.0329636,
+ "lng": 121.5654268
+ },
+ "Penghu County": {
+ "lat": 23.5711899,
+ "lng": 119.5793157
+ },
+ "Lienchiang": {
+ "lat": 26.160243,
+ "lng": 119.9516652
+ },
+ "Hsinchu": {
+ "lat": 24.8138287,
+ "lng": 120.9674798
+ },
+ "Kinmen County": {
+ "lat": 24.34877912595629,
+ "lng": 118.3285644254523
+ },
+ "Keelung": {
+ "lat": 25.1276033,
+ "lng": 121.7391833
+ },
+ "Chiayi": {
+ "lat": 23.4800751,
+ "lng": 120.4491113
+ },
+ "": {
+ "lat": 23.69781,
+ "lng": 120.960515
+ }
+ },
+ "PH": {
+ "Western Visayas": {
+ "lat": 11.0049836,
+ "lng": 122.5372741
+ },
+ "Central Luzon": {
+ "lat": 15.4827722,
+ "lng": 120.7120023
+ },
+ "Zamboanga Peninsula": {
+ "lat": 8.154077,
+ "lng": 123.258793
+ },
+ "Autonomous Region in Muslim Mindanao": {
+ "lat": 6.956831299999999,
+ "lng": 124.2421597
+ },
+ "Bicol": {
+ "lat": 13.4209885,
+ "lng": 123.4136736
+ },
+ "Ilocos": {
+ "lat": 16.0832144,
+ "lng": 120.6199895
+ },
+ "Mimaropa": {
+ "lat": 9.843206499999999,
+ "lng": 118.7364783
+ },
+ "Calabarzon": {
+ "lat": 14.1007803,
+ "lng": 121.0793705
+ },
+ "Metro Manila": {
+ "lat": 14.6090537,
+ "lng": 121.0222565
+ },
+ "Central Visayas": {
+ "lat": 9.816875,
+ "lng": 124.0641419
+ },
+ "Northern Mindanao": {
+ "lat": 8.020163499999999,
+ "lng": 124.6856509
+ },
+ "Cagayan Valley": {
+ "lat": 16.9753758,
+ "lng": 121.8107079
+ },
+ "Soccsksargen": {
+ "lat": 6.2706918,
+ "lng": 124.6856509
+ },
+ "Cordillera": {
+ "lat": 17.3512542,
+ "lng": 121.1718851
+ },
+ "Eastern Visayas": {
+ "lat": 12.2445533,
+ "lng": 125.0388164
+ },
+ "Caraga": {
+ "lat": 8.801456199999999,
+ "lng": 125.7406882
+ },
+ "Davao": {
+ "lat": 7.190708,
+ "lng": 125.455341
+ },
+ "": {
+ "lat": 12.879721,
+ "lng": 121.774017
+ }
+ },
+ "HK": {
+ "Islands District": {
+ "lat": 22.2627924,
+ "lng": 113.9655419
+ },
+ "Yuen Long District": {
+ "lat": 22.4445484,
+ "lng": 114.0222095
+ },
+ "Yau Tsim Mong": {
+ "lat": 22.3116028,
+ "lng": 114.1706884
+ },
+ "Wong Tai Sin": {
+ "lat": 22.3548115,
+ "lng": 114.1974398
+ },
+ "Southern": {
+ "lat": 22.2432164,
+ "lng": 114.1974398
+ },
+ "Wan Chai": {
+ "lat": 22.276022,
+ "lng": 114.1751471
+ },
+ "Eastern": {
+ "lat": 22.2733889,
+ "lng": 114.2360778
+ },
+ "Tuen Mun": {
+ "lat": 22.3908295,
+ "lng": 113.9725126
+ },
+ "Sai Kung District": {
+ "lat": 22.3833893,
+ "lng": 114.270976
+ },
+ "Kwai Tsing": {
+ "lat": 22.3549077,
+ "lng": 114.1260991
+ },
+ "Tsuen Wan District": {
+ "lat": 22.3713227,
+ "lng": 114.1141601
+ },
+ "Kowloon City": {
+ "lat": 22.3232097,
+ "lng": 114.1855505
+ },
+ "Tai Po District": {
+ "lat": 22.4423282,
+ "lng": 114.165521
+ },
+ "Sha Tin": {
+ "lat": 22.3771304,
+ "lng": 114.1974398
+ },
+ "North": {
+ "lat": 22.5009081,
+ "lng": 114.1558258
+ },
+ "Central and Western District": {
+ "lat": 22.2730219,
+ "lng": 114.1498806
+ },
+ "Sham Shui Po": {
+ "lat": 22.3285899,
+ "lng": 114.1602846
+ },
+ "Kwun Tong": {
+ "lat": 22.315698,
+ "lng": 114.2331057
+ },
+ "": {
+ "lat": 22.396428,
+ "lng": 114.109497
+ }
+ },
+ "BN": {
+ "Tutong": {
+ "lat": 4.8140524,
+ "lng": 114.6608562
+ },
+ "Belait": {
+ "lat": 4.3750749,
+ "lng": 114.6192899
+ },
+ "Brunei-Muara District": {
+ "lat": 4.9311206,
+ "lng": 114.9516869
+ },
+ "Temburong": {
+ "lat": 4.6204128,
+ "lng": 115.141484
+ },
+ "": {
+ "lat": 4.535277,
+ "lng": 114.727669
+ }
+ },
+ "KH": {
+ "Phnom Penh": {
+ "lat": 11.5563738,
+ "lng": 104.9282099
+ },
+ "Takeo": {
+ "lat": 10.9877057,
+ "lng": 104.7871004
+ },
+ "Svay Rieng": {
+ "lat": 11.0877866,
+ "lng": 105.800951
+ },
+ "Stung Treng": {
+ "lat": 13.5069103,
+ "lng": 105.9676931
+ },
+ "Siem Reap": {
+ "lat": 13.3632533,
+ "lng": 103.856403
+ },
+ "Mondolkiri": {
+ "lat": 12.7879427,
+ "lng": 107.1011931
+ },
+ "Prey Veng": {
+ "lat": 11.485114,
+ "lng": 105.328098
+ },
+ "Pursat": {
+ "lat": 12.4651824,
+ "lng": 103.8911999
+ },
+ "Kampong Speu": {
+ "lat": 11.4650455,
+ "lng": 104.5072722
+ },
+ "Otar Meanchey": {
+ "lat": 14.1609738,
+ "lng": 103.8216261
+ },
+ "Kampong Chhnang": {
+ "lat": 12.2613926,
+ "lng": 104.6762738
+ },
+ "Tboung Khmum": {
+ "lat": 11.8891023,
+ "lng": 105.876004
+ },
+ "Kandal": {
+ "lat": 11.2237383,
+ "lng": 105.1258955
+ },
+ "Banteay Meanchey": {
+ "lat": 13.7531914,
+ "lng": 102.989615
+ },
+ "Kampong Thom": {
+ "lat": 12.70787,
+ "lng": 104.8736858
+ },
+ "Preah Vihear": {
+ "lat": 14.0085797,
+ "lng": 104.8454619
+ },
+ "Pailin": {
+ "lat": 12.9092962,
+ "lng": 102.6675575
+ },
+ "Kep": {
+ "lat": 10.543246,
+ "lng": 104.319142
+ },
+ "Koh Kong": {
+ "lat": 11.6153795,
+ "lng": 102.9836882
+ },
+ "Kratie": {
+ "lat": 12.4896877,
+ "lng": 106.0287512
+ },
+ "Preah Sihanouk": {
+ "lat": 10.6267867,
+ "lng": 103.5115545
+ },
+ "Kampong Cham": {
+ "lat": 11.9924294,
+ "lng": 105.4645408
+ },
+ "Battambang": {
+ "lat": 13.09573,
+ "lng": 103.2022055
+ },
+ "": {
+ "lat": 12.565679,
+ "lng": 104.990963
+ }
+ },
+ "KR": {
+ "Gyeongsangbuk-do": {
+ "lat": 36.3436011,
+ "lng": 128.7401566
+ },
+ "Daejeon": {
+ "lat": 36.3398175,
+ "lng": 127.3940486
+ },
+ "Gangwon-do": {
+ "lat": 37.724962,
+ "lng": 128.3009629
+ },
+ "Seoul": {
+ "lat": 37.5518911,
+ "lng": 126.9917937
+ },
+ "Jeollanam-do": {
+ "lat": 34.94020010000001,
+ "lng": 126.9565003
+ },
+ "Gyeonggi-do": {
+ "lat": 37.5289145,
+ "lng": 127.1727772
+ },
+ "North Chungcheong": {
+ "lat": 36.7378449,
+ "lng": 127.8305242
+ },
+ "Gyeongsangnam-do": {
+ "lat": 35.36956300000001,
+ "lng": 128.2570135
+ },
+ "Busan": {
+ "lat": 35.2100142,
+ "lng": 129.0688702
+ },
+ "Chungcheongnam-do": {
+ "lat": 36.5296003,
+ "lng": 126.8590621
+ },
+ "Jeollabuk-do": {
+ "lat": 35.7197198,
+ "lng": 127.1243977
+ },
+ "Gwangju": {
+ "lat": 35.1557358,
+ "lng": 126.8354271
+ },
+ "Ulsan": {
+ "lat": 35.5537228,
+ "lng": 129.2380554
+ },
+ "Daegu": {
+ "lat": 35.8294374,
+ "lng": 128.5655119
+ },
+ "Jeju-do": {
+ "lat": 33.3846216,
+ "lng": 126.5534925
+ },
+ "Incheon": {
+ "lat": 37.4562557,
+ "lng": 126.7052062
+ },
+ "Sejong-si": {
+ "lat": 36.5606976,
+ "lng": 127.2587334
+ },
+ "": {
+ "lat": 35.907757,
+ "lng": 127.766922
+ }
+ },
+ "JP": {
+ "Wakayama": {
+ "lat": 34.2303678,
+ "lng": 135.1707405
+ },
+ "Ishikawa": {
+ "lat": 36.3260317,
+ "lng": 136.5289653
+ },
+ "Kanagawa": {
+ "lat": 35.4827827,
+ "lng": 139.6146102
+ },
+ "Hyōgo": {
+ "lat": 34.8579518,
+ "lng": 134.5453787
+ },
+ "Shiga": {
+ "lat": 35.3292014,
+ "lng": 136.0563212
+ },
+ "Kagawa": {
+ "lat": 34.2225915,
+ "lng": 134.0199152
+ },
+ "Shizuoka": {
+ "lat": 34.9755668,
+ "lng": 138.3826773
+ },
+ "Tottori": {
+ "lat": 35.5011082,
+ "lng": 134.2351011
+ },
+ "Gifu": {
+ "lat": 35.42342259999999,
+ "lng": 136.7606217
+ },
+ "Yamagata": {
+ "lat": 38.2554153,
+ "lng": 140.3396175
+ },
+ "Yamaguchi": {
+ "lat": 34.1782945,
+ "lng": 131.4738432
+ },
+ "Fukuoka": {
+ "lat": 33.5901838,
+ "lng": 130.4016888
+ },
+ "Ibaraki": {
+ "lat": 36.2193571,
+ "lng": 140.1832516
+ },
+ "Tokyo": {
+ "lat": 35.6761919,
+ "lng": 139.6503106
+ },
+ "Nara": {
+ "lat": 34.685109,
+ "lng": 135.8048019
+ },
+ "Saitama": {
+ "lat": 35.8616486,
+ "lng": 139.6454782
+ },
+ "Niigata": {
+ "lat": 37.9161244,
+ "lng": 139.0363708
+ },
+ "Nagano": {
+ "lat": 36.6485258,
+ "lng": 138.1950371
+ },
+ "Yamanashi": {
+ "lat": 35.6933453,
+ "lng": 138.6873168
+ },
+ "Okinawa": {
+ "lat": 26.3343533,
+ "lng": 127.8056058
+ },
+ "Okayama": {
+ "lat": 34.6555312,
+ "lng": 133.919795
+ },
+ "Gunma": {
+ "lat": 36.5605388,
+ "lng": 138.8799972
+ },
+ "Mie": {
+ "lat": 33.8143901,
+ "lng": 136.0487047
+ },
+ "Oita": {
+ "lat": 33.2396084,
+ "lng": 131.6095148
+ },
+ "Chiba": {
+ "lat": 35.6074041,
+ "lng": 140.1065366
+ },
+ "Aichi": {
+ "lat": 35.0182505,
+ "lng": 137.2923893
+ },
+ "Kyoto": {
+ "lat": 35.011564,
+ "lng": 135.7681489
+ },
+ "Kumamoto": {
+ "lat": 32.8032164,
+ "lng": 130.7079369
+ },
+ "Shimane": {
+ "lat": 35.1244094,
+ "lng": 132.6293446
+ },
+ "Ōsaka": {
+ "lat": 34.6937249,
+ "lng": 135.5022535
+ },
+ "Kagoshima": {
+ "lat": 31.5968539,
+ "lng": 130.5571392
+ },
+ "Tochigi": {
+ "lat": 36.3823772,
+ "lng": 139.7341396
+ },
+ "Fukui": {
+ "lat": 36.0641386,
+ "lng": 136.219623
+ },
+ "Tokushima": {
+ "lat": 34.0703652,
+ "lng": 134.5549537
+ },
+ "Nagasaki": {
+ "lat": 32.7503334,
+ "lng": 129.8778888
+ },
+ "Hiroshima": {
+ "lat": 34.3852894,
+ "lng": 132.4553055
+ },
+ "Kochi": {
+ "lat": 33.5588821,
+ "lng": 133.5312383
+ },
+ "Toyama": {
+ "lat": 36.6958223,
+ "lng": 137.2137211
+ },
+ "Miyazaki": {
+ "lat": 31.9077285,
+ "lng": 131.4202196
+ },
+ "Saga": {
+ "lat": 33.2631179,
+ "lng": 130.3009057
+ },
+ "Akita": {
+ "lat": 39.7199668,
+ "lng": 140.1034795
+ },
+ "Fukushima": {
+ "lat": 37.7607991,
+ "lng": 140.4747856
+ },
+ "Ehime": {
+ "lat": 33.6025306,
+ "lng": 132.7857583
+ },
+ "Miyagi": {
+ "lat": 38.630612,
+ "lng": 141.1193048
+ },
+ "Iwate": {
+ "lat": 39.9726552,
+ "lng": 141.2124895
+ },
+ "Hokkaido": {
+ "lat": 43.2203266,
+ "lng": 142.8634737
+ },
+ "Aomori": {
+ "lat": 40.8222197,
+ "lng": 140.7473524
+ },
+ "": {
+ "lat": 36.204824,
+ "lng": 138.252924
+ }
+ },
+ "KP": {
+ "Pyongyang": {
+ "lat": 39.0737987,
+ "lng": 125.8197642
+ },
+ "Chagang-do": {
+ "lat": 40.7202809,
+ "lng": 126.5621137
+ },
+ "": {
+ "lat": 40.339852,
+ "lng": 127.510093
+ }
+ },
+ "AU": {
+ "South Australia": {
+ "lat": -30.0002315,
+ "lng": 136.2091547
+ },
+ "Western Australia": {
+ "lat": -27.6728168,
+ "lng": 121.6283098
+ },
+ "Northern Territory": {
+ "lat": -19.4914108,
+ "lng": 132.5509603
+ },
+ "Queensland": {
+ "lat": -22.575197,
+ "lng": 144.0847926
+ },
+ "New South Wales": {
+ "lat": -31.2532183,
+ "lng": 146.921099
+ },
+ "Victoria": {
+ "lat": -36.9847807,
+ "lng": 143.3906074
+ },
+ "Tasmania": {
+ "lat": -42.0409059,
+ "lng": 146.8087322
+ },
+ "Australian Capital Territory": {
+ "lat": -35.4734679,
+ "lng": 149.0123679
+ },
+ "": {
+ "lat": -25.274398,
+ "lng": 133.775136
+ }
+ },
+ "FM": {
+ "State of Chuuk": {
+ "lat": 7.134462467372186,
+ "lng": 151.5074133217651
+ },
+ "State of Kosrae": {
+ "lat": 7.425554,
+ "lng": 150.550812
+ },
+ "State of Pohnpei": {
+ "lat": 6.8518697,
+ "lng": 158.2146857
+ },
+ "State of Yap": {
+ "lat": 9.5556503,
+ "lng": 138.1399232
+ },
+ "": {
+ "lat": 7.425554,
+ "lng": 150.550812
+ }
+ },
+ "PG": {
+ "East Sepik Province": {
+ "lat": -4.3150058,
+ "lng": 143.045893
+ },
+ "Enga Province": {
+ "lat": -5.3005849,
+ "lng": 143.5635637
+ },
+ "West Sepik Province": {
+ "lat": -3.7126179,
+ "lng": 141.6834275
+ },
+ "East New Britain Province": {
+ "lat": -4.612894300000001,
+ "lng": 151.8877321
+ },
+ "National Capital": {
+ "lat": -9.4377025,
+ "lng": 147.2141209
+ },
+ "Northern Province": {
+ "lat": -8.8988063,
+ "lng": 148.1892921
+ },
+ "Bougainville": {
+ "lat": -6.053602,
+ "lng": 155.1907309
+ },
+ "Gulf Province": {
+ "lat": -7.1445402,
+ "lng": 143.7369154
+ },
+ "Western Highlands Province": {
+ "lat": -5.8310333,
+ "lng": 144.1720041
+ },
+ "New Ireland": {
+ "lat": -4.2853256,
+ "lng": 152.9205918
+ },
+ "Southern Highlands Province": {
+ "lat": -6.4179083,
+ "lng": 143.5635637
+ },
+ "Madang Province": {
+ "lat": -4.984973300000001,
+ "lng": 145.1375834
+ },
+ "Manus Province": {
+ "lat": -2.0941169,
+ "lng": 146.8760951
+ },
+ "Morobe Province": {
+ "lat": -6.801373700000001,
+ "lng": 146.561647
+ },
+ "Chimbu Province": {
+ "lat": -6.308768199999999,
+ "lng": 144.8731219
+ },
+ "Central Province": {
+ "lat": -9.1360187,
+ "lng": 147.4627259
+ },
+ "Hela": {
+ "lat": -5.5029613,
+ "lng": 142.5318604
+ },
+ "West New Britain Province": {
+ "lat": -5.704743199999999,
+ "lng": 150.0259466
+ },
+ "Eastern Highlands Province": {
+ "lat": -6.5861674,
+ "lng": 145.6689636
+ },
+ "Western Province": {
+ "lat": -7.8344739,
+ "lng": 142.0215415
+ },
+ "Milne Bay Province": {
+ "lat": -9.5221451,
+ "lng": 150.6749653
+ },
+ "": {
+ "lat": -6.314993,
+ "lng": 143.95555
+ }
+ },
+ "SB": {
+ "Central Province": {
+ "lat": -9.0516057,
+ "lng": 160.1693949
+ },
+ "Western Province": {
+ "lat": -8.128037299999999,
+ "lng": 157.4278119
+ },
+ "Guadalcanal Province": {
+ "lat": -9.577328399999999,
+ "lng": 160.1455805
+ },
+ "Honiara": {
+ "lat": -9.4456381,
+ "lng": 159.9728999
+ },
+ "Isabel Province": {
+ "lat": -8.0592353,
+ "lng": 159.1447081
+ },
+ "Malaita Province": {
+ "lat": -8.9446168,
+ "lng": 160.9071236
+ },
+ "Rennell and Bellona": {
+ "lat": -11.6598657,
+ "lng": 160.2170209
+ },
+ "Temotu Province": {
+ "lat": -10.7309282,
+ "lng": 166.0623979
+ },
+ "Makira-Ulawa Province": {
+ "lat": -10.5737447,
+ "lng": 161.8096941
+ },
+ "Choiseul": {
+ "lat": -7.0501494,
+ "lng": 156.9511459
+ },
+ "": {
+ "lat": -9.64571,
+ "lng": 160.156194
+ }
+ },
+ "KI": {
+ "Gilbert Islands": {
+ "lat": 0,
+ "lng": 174
+ },
+ "Line Islands": {
+ "lat": -3.37344501173228,
+ "lng": -155.300819350443
+ },
+ "Phoenix Islands": {
+ "lat": -3.730110963939701,
+ "lng": -172.6253624186994
+ },
+ "": {
+ "lat": -3.370417,
+ "lng": -168.734039
+ }
+ },
+ "TV": {
+ "Nui": {
+ "lat": -7.238876800000001,
+ "lng": 177.1485232
+ },
+ "Funafuti": {
+ "lat": -8.5211471,
+ "lng": 179.1961926
+ },
+ "Vaitupu": {
+ "lat": -7.476732699999999,
+ "lng": 178.6747675
+ },
+ "Nukulaelae": {
+ "lat": -9.381110999999999,
+ "lng": 179.852222
+ },
+ "Nanumea": {
+ "lat": -5.6613889,
+ "lng": 176.1036111
+ },
+ "Nanumanga": {
+ "lat": -6.2858019,
+ "lng": 176.319928
+ },
+ "Niutao": {
+ "lat": -6.106425799999999,
+ "lng": 177.3438429
+ },
+ "Nukufetau": {
+ "lat": -8,
+ "lng": 178.5
+ },
+ "": {
+ "lat": -7.109535,
+ "lng": 177.64933
+ }
+ },
+ "NR": {
+ "Aiwo": {
+ "lat": -0.5340012,
+ "lng": 166.9138873
+ },
+ "Uaboe": {
+ "lat": -0.5202222,
+ "lng": 166.9311761
+ },
+ "Anetan": {
+ "lat": -0.5064343,
+ "lng": 166.9427006
+ },
+ "Baiti": {
+ "lat": -0.5133158,
+ "lng": 166.9311761
+ },
+ "Ijuw": {
+ "lat": -0.5202646000000001,
+ "lng": 166.9513432
+ },
+ "Ewa": {
+ "lat": -0.5087241,
+ "lng": 166.9369384
+ },
+ "Anibare": {
+ "lat": -0.532915,
+ "lng": 166.944141
+ },
+ "Anabar": {
+ "lat": -0.5133517,
+ "lng": 166.9484624
+ },
+ "Yaren": {
+ "lat": -0.5466856999999999,
+ "lng": 166.9210913
+ },
+ "Buada": {
+ "lat": -0.5328777,
+ "lng": 166.9268541
+ },
+ "Boe": {
+ "lat": -0.5414945,
+ "lng": 166.9174893
+ },
+ "Meneng": {
+ "lat": -0.546724,
+ "lng": 166.938379
+ },
+ "Nibok": {
+ "lat": -0.5225124,
+ "lng": 166.9254134
+ },
+ "Denigomodu": {
+ "lat": -0.5247963999999999,
+ "lng": 166.9167689
+ },
+ "": {
+ "lat": -0.522778,
+ "lng": 166.931503
+ }
+ },
+ "MH": {
+ "Majuro Atoll": {
+ "lat": 7.066667,
+ "lng": 171.266667
+ },
+ "Kwajalein Atoll": {
+ "lat": 8.716667,
+ "lng": 167.733333
+ },
+ "Wotho Atoll": {
+ "lat": 10.112915,
+ "lng": 165.9695465
+ },
+ "": {
+ "lat": 7.131474,
+ "lng": 171.184478
+ }
+ },
+ "VU": {
+ "Torba": {
+ "lat": -14.28096,
+ "lng": 167.5160788
+ },
+ "Shefa Province": {
+ "lat": -17.6523412,
+ "lng": 168.3387066
+ },
+ "Malampa Province": {
+ "lat": -16.3031816,
+ "lng": 167.5160788
+ },
+ "Sanma Province": {
+ "lat": -15.3003549,
+ "lng": 166.9182097
+ },
+ "Penama Province": {
+ "lat": -15.3335375,
+ "lng": 167.9053182
+ },
+ "Tafea Province": {
+ "lat": -18.8175345,
+ "lng": 169.0645056
+ },
+ "": {
+ "lat": -15.376706,
+ "lng": 166.959158
+ }
+ },
+ "NC": {
+ "Loyalty Islands": {
+ "lat": -21.0454468,
+ "lng": 167.3324425
+ },
+ "North Province": {
+ "lat": -20.8888373,
+ "lng": 164.8741045
+ },
+ "South Province": {
+ "lat": -21.7482006,
+ "lng": 166.1783739
+ },
+ "": {
+ "lat": -20.904305,
+ "lng": 165.618042
+ }
+ },
+ "NZ": {
+ "Auckland": {
+ "lat": -36.85088270000001,
+ "lng": 174.7644881
+ },
+ "Southland": {
+ "lat": -45.84891589999999,
+ "lng": 167.6755387
+ },
+ "Canterbury": {
+ "lat": -43.7542275,
+ "lng": 171.1637245
+ },
+ "Marlborough": {
+ "lat": -41.5916883,
+ "lng": 173.7624053
+ },
+ "Otago": {
+ "lat": -45.47906709999999,
+ "lng": 170.1547567
+ },
+ "Waikato": {
+ "lat": -37.6190862,
+ "lng": 175.023346
+ },
+ "Gisborne": {
+ "lat": -38.6640913,
+ "lng": 178.0227931
+ },
+ "Bay of Plenty": {
+ "lat": -37.4233917,
+ "lng": 176.7416374
+ },
+ "Wellington": {
+ "lat": -41.2923814,
+ "lng": 174.7787463
+ },
+ "Taranaki": {
+ "lat": -39.3538149,
+ "lng": 174.4382721
+ },
+ "Manawatu-Wanganui": {
+ "lat": -39.9330715,
+ "lng": 175.0286083
+ },
+ "Tasman": {
+ "lat": -41.4571184,
+ "lng": 172.820974
+ },
+ "Northland": {
+ "lat": -35.4136172,
+ "lng": 173.9320806
+ },
+ "Hawke's Bay": {
+ "lat": -39.6016597,
+ "lng": 176.5804473
+ },
+ "West Coast": {
+ "lat": -42.6919232,
+ "lng": 171.3399414
+ },
+ "Nelson": {
+ "lat": -41.2985321,
+ "lng": 173.2443635
+ },
+ "Chatham Islands": {
+ "lat": -43.9271098,
+ "lng": -176.4592091
+ },
+ "": {
+ "lat": -40.900557,
+ "lng": 174.885971
+ }
+ },
+ "FJ": {
+ "Central": {
+ "lat": -17.9453123,
+ "lng": 178.2461183
+ },
+ "Western": {
+ "lat": -17.7510845,
+ "lng": 177.7763333
+ },
+ "Northern": {
+ "lat": -16.6268225,
+ "lng": 179.0179332
+ },
+ "Rotuma": {
+ "lat": -12.5025069,
+ "lng": 177.0724164
+ },
+ "": {
+ "lat": -16.578193,
+ "lng": 179.414413
+ }
+ },
+ "CM": {
+ "Centre": {
+ "lat": 4.6298411,
+ "lng": 11.7068294
+ },
+ "North-West Region": {
+ "lat": 6.470373899999999,
+ "lng": 10.439656
+ },
+ "Adamaoua Region": {
+ "lat": 6.9181954,
+ "lng": 12.8054753
+ },
+ "Littoral": {
+ "lat": 4.1682138,
+ "lng": 10.0807298
+ },
+ "Far North Region": {
+ "lat": 10.6315589,
+ "lng": 14.6587821
+ },
+ "South": {
+ "lat": 2.7202832,
+ "lng": 11.7068294
+ },
+ "South-West Region": {
+ "lat": 5.1573493,
+ "lng": 9.367308399999999
+ },
+ "West Region": {
+ "lat": 5.4638158,
+ "lng": 10.8000051
+ },
+ "": {
+ "lat": 7.369722,
+ "lng": 12.354722
+ }
+ },
+ "SN": {
+ "Ziguinchor": {
+ "lat": 12.5641479,
+ "lng": -16.2639825
+ },
+ "Region de Thies": {
+ "lat": 14.7910052,
+ "lng": -16.9358604
+ },
+ "Region de Sedhiou": {
+ "lat": 12.8836881,
+ "lng": -15.5943388
+ },
+ "Fatick": {
+ "lat": 14.3390167,
+ "lng": -16.4111425
+ },
+ "Matam": {
+ "lat": 15.6600225,
+ "lng": -13.2576906
+ },
+ "Diourbel": {
+ "lat": 14.6561238,
+ "lng": -16.2345633
+ },
+ "Saint-Louis": {
+ "lat": 16.0326307,
+ "lng": -16.4818167
+ },
+ "Dakar": {
+ "lat": 14.716677,
+ "lng": -17.4676861
+ },
+ "Region de Kedougou": {
+ "lat": 12.5604607,
+ "lng": -12.1747077
+ },
+ "Kaolack": {
+ "lat": 14.1652083,
+ "lng": -16.0757749
+ },
+ "Louga": {
+ "lat": 15.6141768,
+ "lng": -16.22868
+ },
+ "Kolda": {
+ "lat": 12.9001718,
+ "lng": -14.9425368
+ },
+ "Tambacounda": {
+ "lat": 13.7725888,
+ "lng": -13.6710059
+ },
+ "Region de Kaffrine": {
+ "lat": 14.3466477,
+ "lng": -15.2194808
+ },
+ "": {
+ "lat": 14.497401,
+ "lng": -14.452362
+ }
+ },
+ "CG": {
+ "Lékoumou": {
+ "lat": -3.170382,
+ "lng": 13.3587288
+ },
+ "Pointe-Noire": {
+ "lat": -4.7691623,
+ "lng": 11.866362
+ },
+ "Sangha": {
+ "lat": 1.4662328,
+ "lng": 15.4068079
+ },
+ "Cuvette": {
+ "lat": -0.2877446,
+ "lng": 16.1580937
+ },
+ "Niari": {
+ "lat": -3.18427,
+ "lng": 12.2547919
+ },
+ "Brazzaville": {
+ "lat": -4.2744405,
+ "lng": 15.2812803
+ },
+ "Likouala": {
+ "lat": 2.043924000000001,
+ "lng": 17.668887
+ },
+ "Kouilou": {
+ "lat": -4.1428413,
+ "lng": 11.8891721
+ },
+ "Cuvette-Ouest": {
+ "lat": 0.144755,
+ "lng": 14.4723301
+ },
+ "Plateaux": {
+ "lat": -2.0680088,
+ "lng": 15.4068079
+ },
+ "Pool": {
+ "lat": -3.7762628,
+ "lng": 14.8454619
+ },
+ "": {
+ "lat": -0.228021,
+ "lng": 15.827659
+ }
+ },
+ "PT": {
+ "Santarém": {
+ "lat": 39.2848804,
+ "lng": -8.704075
+ },
+ "Beja": {
+ "lat": 38.0153039,
+ "lng": -7.8627308
+ },
+ "Évora": {
+ "lat": 38.571431,
+ "lng": -7.913501999999999
+ },
+ "Lisbon": {
+ "lat": 38.7222524,
+ "lng": -9.1393366
+ },
+ "Faro": {
+ "lat": 37.0193548,
+ "lng": -7.9304397
+ },
+ "Setúbal": {
+ "lat": 38.5260437,
+ "lng": -8.8909328
+ },
+ "Bragança": {
+ "lat": 41.8061131,
+ "lng": -6.756737999999999
+ },
+ "Castelo Branco": {
+ "lat": 39.8197117,
+ "lng": -7.4964662
+ },
+ "Leiria": {
+ "lat": 39.74953310000001,
+ "lng": -8.807682999999999
+ },
+ "Portalegre": {
+ "lat": 39.2967086,
+ "lng": -7.4284755
+ },
+ "Madeira": {
+ "lat": 32.76070740000001,
+ "lng": -16.9594723
+ },
+ "Viseu": {
+ "lat": 40.6588424,
+ "lng": -7.914756
+ },
+ "Braga": {
+ "lat": 41.5454486,
+ "lng": -8.426506999999999
+ },
+ "Porto": {
+ "lat": 41.1579438,
+ "lng": -8.629105299999999
+ },
+ "Vila Real": {
+ "lat": 41.3010351,
+ "lng": -7.7422354
+ },
+ "Guarda": {
+ "lat": 40.5308408,
+ "lng": -7.2221421
+ },
+ "Viana do Castelo": {
+ "lat": 41.6918275,
+ "lng": -8.8344101
+ },
+ "Coimbra": {
+ "lat": 40.2033145,
+ "lng": -8.4102573
+ },
+ "Aveiro": {
+ "lat": 40.6405055,
+ "lng": -8.6537539
+ },
+ "Azores": {
+ "lat": 37.7412488,
+ "lng": -25.6755944
+ },
+ "": {
+ "lat": 39.399872,
+ "lng": -8.224454
+ }
+ },
+ "LR": {
+ "Grand Gedeh County": {
+ "lat": 5.9222078,
+ "lng": -8.2212979
+ },
+ "Grand Kru County": {
+ "lat": 4.7613862,
+ "lng": -8.2212979
+ },
+ "Lofa County": {
+ "lat": 8.191118399999999,
+ "lng": -9.7232673
+ },
+ "Bomi County": {
+ "lat": 6.7171372,
+ "lng": -10.7097867
+ },
+ "Gbarpolu County": {
+ "lat": 7.495263700000001,
+ "lng": -10.0807298
+ },
+ "Nimba County": {
+ "lat": 6.8427612,
+ "lng": -8.6600586
+ },
+ "River Cess County": {
+ "lat": 5.8205993,
+ "lng": -9.367308399999999
+ },
+ "Montserrado County": {
+ "lat": 6.509587199999999,
+ "lng": -10.5746224
+ },
+ "Margibi County": {
+ "lat": 6.5151875,
+ "lng": -10.3048897
+ },
+ "Maryland County": {
+ "lat": 4.6033516,
+ "lng": -7.6982272
+ },
+ "Sinoe County": {
+ "lat": 5.49871,
+ "lng": -8.6600586
+ },
+ "Bong County": {
+ "lat": 6.908980300000001,
+ "lng": -9.634134999999999
+ },
+ "Grand Bassa County": {
+ "lat": 6.2308452,
+ "lng": -9.8124935
+ },
+ "": {
+ "lat": 6.428055,
+ "lng": -9.429499
+ }
+ },
+ "CI": {
+ "Yamoussoukro Autonomous District": {
+ "lat": 6.827622799999999,
+ "lng": -5.2893433
+ },
+ "Lacs": {
+ "lat": 6.899194400000001,
+ "lng": -4.5624426
+ },
+ "Abidjan": {
+ "lat": 5.3599517,
+ "lng": -4.0082563
+ },
+ "Zanzan": {
+ "lat": 8.8207904,
+ "lng": -3.4195527
+ },
+ "Bas-Sassandra": {
+ "lat": 5.5253377,
+ "lng": -6.5783387
+ },
+ "Goh-Djiboua": {
+ "lat": 5.871139299999999,
+ "lng": -5.5617279
+ },
+ "Denguele": {
+ "lat": 9.646674299999999,
+ "lng": -7.265285799999999
+ },
+ "Woroba": {
+ "lat": 8.2548962,
+ "lng": -6.5783387
+ },
+ "Montagnes": {
+ "lat": 7.376237300000001,
+ "lng": -7.4381355
+ },
+ "Savanes": {
+ "lat": 9.6783614,
+ "lng": -5.5617279
+ },
+ "Comoe": {
+ "lat": 5.974122899999999,
+ "lng": -3.1779659
+ },
+ "Sassandra-Marahoue": {
+ "lat": 7.222729999999999,
+ "lng": -6.2375947
+ },
+ "Lagunes": {
+ "lat": 5.882733399999999,
+ "lng": -4.2333355
+ },
+ "Vallee du Bandama": {
+ "lat": 8.278978,
+ "lng": -4.893562699999999
+ },
+ "": {
+ "lat": 7.539989,
+ "lng": -5.54708
+ }
+ },
+ "GH": {
+ "Northern Region": {
+ "lat": 9.367277099999999,
+ "lng": -0.1494988
+ },
+ "Upper West Region": {
+ "lat": 10.2529757,
+ "lng": -2.1450245
+ },
+ "Greater Accra Region": {
+ "lat": 5.8142836,
+ "lng": 0.0746767
+ },
+ "Ashanti Region": {
+ "lat": 6.7470436,
+ "lng": -1.5208624
+ },
+ "Western Region": {
+ "lat": 5.5572659,
+ "lng": -2.302446
+ },
+ "Bono": {
+ "lat": 7.946527,
+ "lng": -1.023194
+ },
+ "Eastern Region": {
+ "lat": 6.5781373,
+ "lng": -0.4502368
+ },
+ "Central Region": {
+ "lat": 5.644375399999999,
+ "lng": -1.2891036
+ },
+ "Western North": {
+ "lat": 5.5572659,
+ "lng": -2.302446
+ },
+ "Upper East Region": {
+ "lat": 10.7982051,
+ "lng": -1.0586135
+ },
+ "North East": {
+ "lat": 7.946527,
+ "lng": -1.023194
+ },
+ "Ahafo": {
+ "lat": 7.9559247,
+ "lng": -1.6760691
+ },
+ "Bono East": {
+ "lat": 6.5781373,
+ "lng": -0.4502368
+ },
+ "Volta Region": {
+ "lat": 6.5781373,
+ "lng": 0.4502368
+ },
+ "Oti": {
+ "lat": 9.172306030466055,
+ "lng": 0.2624567475647809
+ },
+ "Savannah": {
+ "lat": 7.946527,
+ "lng": -1.023194
+ },
+ "": {
+ "lat": 7.946527,
+ "lng": -1.023194
+ }
+ },
+ "GQ": {
+ "Annobon": {
+ "lat": -1.4268782,
+ "lng": 5.6352801
+ },
+ "Wele-Nzas": {
+ "lat": 1.4166162,
+ "lng": 11.0711758
+ },
+ "Kié-Ntem": {
+ "lat": 2.028093,
+ "lng": 11.0711758
+ },
+ "Centro Sur": {
+ "lat": 1.3436084,
+ "lng": 10.439656
+ },
+ "Bioko Norte": {
+ "lat": 3.6595072,
+ "lng": 8.7921836
+ },
+ "Bioko Sur": {
+ "lat": 3.4209785,
+ "lng": 8.6160674
+ },
+ "Litoral": {
+ "lat": 1.5750244,
+ "lng": 9.8124935
+ },
+ "Djibloho": {
+ "lat": 1.616835,
+ "lng": 10.82757
+ },
+ "": {
+ "lat": 1.650801,
+ "lng": 10.267895
+ }
+ },
+ "NG": {
+ "Kaduna State": {
+ "lat": 10.3764006,
+ "lng": 7.7094537
+ },
+ "Adamawa": {
+ "lat": 9.3264751,
+ "lng": 12.3983853
+ },
+ "Lagos": {
+ "lat": 6.5243793,
+ "lng": 3.3792057
+ },
+ "Taraba State": {
+ "lat": 7.9993616,
+ "lng": 10.7739863
+ },
+ "Delta": {
+ "lat": 5.704030800000001,
+ "lng": 5.9339181
+ },
+ "Nasarawa State": {
+ "lat": 8.4997908,
+ "lng": 8.1996937
+ },
+ "Akwa Ibom State": {
+ "lat": 4.9057371,
+ "lng": 7.853667499999999
+ },
+ "Abia State": {
+ "lat": 5.4527354,
+ "lng": 7.5248414
+ },
+ "Borno State": {
+ "lat": 11.8846356,
+ "lng": 13.1519665
+ },
+ "Yobe State": {
+ "lat": 12.293876,
+ "lng": 11.4390411
+ },
+ "Sokoto State": {
+ "lat": 13.0533143,
+ "lng": 5.3222722
+ },
+ "Ogun State": {
+ "lat": 6.997974699999999,
+ "lng": 3.4737378
+ },
+ "Rivers State": {
+ "lat": 4.8396414,
+ "lng": 6.911237799999999
+ },
+ "Gombe State": {
+ "lat": 10.3637795,
+ "lng": 11.1927587
+ },
+ "Oyo State": {
+ "lat": 8.157380900000001,
+ "lng": 3.6146534
+ },
+ "Imo State": {
+ "lat": 5.5720122,
+ "lng": 7.0588219
+ },
+ "Osun State": {
+ "lat": 7.562896399999999,
+ "lng": 4.5199593
+ },
+ "Kwara State": {
+ "lat": 8.9668961,
+ "lng": 4.3874051
+ },
+ "Anambra": {
+ "lat": 6.220899699999999,
+ "lng": 6.9369559
+ },
+ "Ondo State": {
+ "lat": 6.9148682,
+ "lng": 5.1478144
+ },
+ "Kogi State": {
+ "lat": 7.7337325,
+ "lng": 6.6905836
+ },
+ "Enugu State": {
+ "lat": 6.536353,
+ "lng": 7.435619399999999
+ },
+ "Cross River State": {
+ "lat": 5.8701724,
+ "lng": 8.5988014
+ },
+ "FCT": {
+ "lat": 8.8940691,
+ "lng": 7.1860402
+ },
+ "Niger State": {
+ "lat": 9.930922400000002,
+ "lng": 5.598320999999999
+ },
+ "Plateau State": {
+ "lat": 9.2182093,
+ "lng": 9.5179488
+ },
+ "Katsina State": {
+ "lat": 12.3796707,
+ "lng": 7.6305748
+ },
+ "Kano State": {
+ "lat": 11.7470698,
+ "lng": 8.5247107
+ },
+ "Kebbi": {
+ "lat": 11.4942003,
+ "lng": 4.2333355
+ },
+ "Ebonyi State": {
+ "lat": 6.2649232,
+ "lng": 8.0137302
+ },
+ "Edo": {
+ "lat": 6.6341831,
+ "lng": 5.9304056
+ },
+ "Bauchi": {
+ "lat": 10.3059966,
+ "lng": 9.840395899999999
+ },
+ "Zamfara State": {
+ "lat": 12.1221805,
+ "lng": 6.2235819
+ },
+ "Ekiti State": {
+ "lat": 7.718986200000001,
+ "lng": 5.310950500000001
+ },
+ "Benue State": {
+ "lat": 7.3369024,
+ "lng": 8.7403687
+ },
+ "Bayelsa State": {
+ "lat": 4.7719071,
+ "lng": 6.0698526
+ },
+ "": {
+ "lat": 9.081999,
+ "lng": 8.675277
+ }
+ },
+ "BF": {
+ "Plateau-Central": {
+ "lat": 12.2537648,
+ "lng": -0.7532808999999999
+ },
+ "Centre-Est": {
+ "lat": 11.5247674,
+ "lng": -0.1494988
+ },
+ "Nord": {
+ "lat": 13.718252,
+ "lng": -2.302446
+ },
+ "Centre": {
+ "lat": 12.3425897,
+ "lng": -1.443469
+ },
+ "Centre-Sud": {
+ "lat": 11.5228911,
+ "lng": -1.0586135
+ },
+ "Boucle du Mouhoun": {
+ "lat": 13.1299154,
+ "lng": -3.3693702
+ },
+ "Centre-Nord": {
+ "lat": 13.1724464,
+ "lng": -0.9056623
+ },
+ "Sahel": {
+ "lat": 12.238333,
+ "lng": -1.561593
+ },
+ "Sud-Ouest": {
+ "lat": 10.4231493,
+ "lng": -3.2583626
+ },
+ "Est": {
+ "lat": 12.4365526,
+ "lng": 0.9056623
+ },
+ "Hauts-Bassins": {
+ "lat": 11.4942003,
+ "lng": -4.2333355
+ },
+ "Cascades Region": {
+ "lat": 10.4072992,
+ "lng": -4.5624426
+ },
+ "": {
+ "lat": 12.238333,
+ "lng": -1.561593
+ }
+ },
+ "TG": {
+ "Centrale": {
+ "lat": 8.6586029,
+ "lng": 1.0586135
+ },
+ "Savanes": {
+ "lat": 10.5291781,
+ "lng": 0.5257822999999999
+ },
+ "Maritime": {
+ "lat": 6.4913504,
+ "lng": 1.2891036
+ },
+ "Plateaux": {
+ "lat": 7.6101378,
+ "lng": 1.0586135
+ },
+ "Kara": {
+ "lat": 9.5486112,
+ "lng": 1.1977158
+ },
+ "": {
+ "lat": 8.619543,
+ "lng": 0.824782
+ }
+ },
+ "GW": {
+ "Biombo": {
+ "lat": 11.8529061,
+ "lng": -15.7351171
+ },
+ "Gabu": {
+ "lat": 12.2836137,
+ "lng": -14.2251939
+ },
+ "Cacheu Region": {
+ "lat": 12.0551416,
+ "lng": -16.0640179
+ },
+ "Bafata": {
+ "lat": 12.1720266,
+ "lng": -14.6568929
+ },
+ "Bolama": {
+ "lat": 11.5770712,
+ "lng": -15.4800382
+ },
+ "Quinara": {
+ "lat": 11.7904668,
+ "lng": -15.2662931
+ },
+ "Oio Region": {
+ "lat": 12.2760709,
+ "lng": -15.3131185
+ },
+ "Bissau": {
+ "lat": 11.803749,
+ "lng": -15.180413
+ },
+ "": {
+ "lat": 11.803749,
+ "lng": -15.180413
+ }
+ },
+ "MR": {
+ "Tagant": {
+ "lat": 18.5467527,
+ "lng": -9.9018131
+ },
+ "Hodh El Gharbi": {
+ "lat": 16.6912149,
+ "lng": -9.5450974
+ },
+ "Trarza": {
+ "lat": 17.8664964,
+ "lng": -14.6587821
+ },
+ "Hodh Ech Chargi": {
+ "lat": 18.6737026,
+ "lng": -7.092877
+ },
+ "Gorgol": {
+ "lat": 15.949622,
+ "lng": -12.989615
+ },
+ "Assaba": {
+ "lat": 16.579953,
+ "lng": -11.7068294
+ },
+ "Tiris Zemmour": {
+ "lat": 24.5773764,
+ "lng": -9.9018131
+ },
+ "Guidimaka": {
+ "lat": 15.2557331,
+ "lng": -12.2547919
+ },
+ "Adrar": {
+ "lat": 21.6398783,
+ "lng": -8.4842465
+ },
+ "Brakna": {
+ "lat": 17.4047323,
+ "lng": -13.3587288
+ },
+ "Inchiri": {
+ "lat": 19.5195792,
+ "lng": -14.8454619
+ },
+ "Nouakchott Sud": {
+ "lat": 18.0130087,
+ "lng": -15.993491
+ },
+ "Nouakchott Ouest": {
+ "lat": 18.1050859,
+ "lng": -15.993491
+ },
+ "": {
+ "lat": 21.00789,
+ "lng": -10.940835
+ }
+ },
+ "BJ": {
+ "Collines Department": {
+ "lat": 8.3022297,
+ "lng": 2.302446
+ },
+ "Atakora Department": {
+ "lat": 10.7954931,
+ "lng": 1.6760691
+ },
+ "Ouémé": {
+ "lat": 6.6148152,
+ "lng": 2.4999918
+ },
+ "Borgou Department": {
+ "lat": 9.5340864,
+ "lng": 2.7779813
+ },
+ "Atlantique Department": {
+ "lat": 6.6588391,
+ "lng": 2.2236667
+ },
+ "Alibori": {
+ "lat": 10.9681093,
+ "lng": 2.7779813
+ },
+ "Mono": {
+ "lat": 6.6607182,
+ "lng": 1.7538817
+ },
+ "Kouffo Department": {
+ "lat": 7.003589400000001,
+ "lng": 1.7538817
+ },
+ "Donga": {
+ "lat": 9.7191867,
+ "lng": 1.6760691
+ },
+ "Littoral": {
+ "lat": 6.3806973,
+ "lng": 2.4406387
+ },
+ "Zou Department": {
+ "lat": 7.346926799999999,
+ "lng": 2.0665197
+ },
+ "": {
+ "lat": 9.30769,
+ "lng": 2.315834
+ }
+ },
+ "GA": {
+ "Ogooué-Maritime": {
+ "lat": -1.3465975,
+ "lng": 9.7232673
+ },
+ "Woleu-Ntem": {
+ "lat": 1.4892408,
+ "lng": 11.7068294
+ },
+ "Moyen-Ogooué": {
+ "lat": -0.442784,
+ "lng": 10.439656
+ },
+ "Nyanga": {
+ "lat": -2.8821033,
+ "lng": 11.1617356
+ },
+ "Ngouni": {
+ "lat": -1.4930303,
+ "lng": 10.9807003
+ },
+ "Ogooué-Ivindo": {
+ "lat": 0.8818311,
+ "lng": 13.1740348
+ },
+ "Estuaire": {
+ "lat": 0.4432864,
+ "lng": 10.0807298
+ },
+ "Ogooué-Lolo": {
+ "lat": -0.8844093,
+ "lng": 12.4380581
+ },
+ "Haut-Ogooué": {
+ "lat": -1.4762544,
+ "lng": 13.914399
+ },
+ "": {
+ "lat": -0.803689,
+ "lng": 11.609444
+ }
+ },
+ "SL": {
+ "Western Area": {
+ "lat": 8.3114983,
+ "lng": -13.035694
+ },
+ "North West": {
+ "lat": 8.7985396,
+ "lng": -12.6216211
+ },
+ "Northern Province": {
+ "lat": 9.1816965,
+ "lng": -11.5248055
+ },
+ "Eastern Province": {
+ "lat": 7.8106095,
+ "lng": -11.1617356
+ },
+ "Southern Province": {
+ "lat": 7.790662499999999,
+ "lng": -11.8891721
+ },
+ "": {
+ "lat": 8.460555,
+ "lng": -11.779889
+ }
+ },
+ "ST": {
+ "Agua Grande": {
+ "lat": 0.3395496,
+ "lng": 6.7119409
+ },
+ "Principe": {
+ "lat": 1.6139012,
+ "lng": 7.405702400000001
+ },
+ "Lemba": {
+ "lat": 0.2416603,
+ "lng": 6.5143087
+ },
+ "": {
+ "lat": 0.18636,
+ "lng": 6.613081
+ }
+ },
+ "SH": {
+ "Ascension": {
+ "lat": -7.9438072,
+ "lng": -14.3741107
+ },
+ "Tristan da Cunha": {
+ "lat": -37.1052489,
+ "lng": -12.2776838
+ },
+ "Saint Helena": {
+ "lat": -15.9650104,
+ "lng": -5.7089241
+ },
+ "": {
+ "lat": -24.143474,
+ "lng": -10.030696
+ }
+ },
+ "GM": {
+ "West Coast": {
+ "lat": 13.2229202,
+ "lng": -16.5819789
+ },
+ "Lower River Division": {
+ "lat": 13.3553306,
+ "lng": -15.92299
+ },
+ "Banjul": {
+ "lat": 13.454375,
+ "lng": -16.5753186
+ },
+ "Central River": {
+ "lat": 13.5994469,
+ "lng": -14.8921668
+ },
+ "North Bank": {
+ "lat": 13.5285436,
+ "lng": -16.0169971
+ },
+ "": {
+ "lat": 13.443182,
+ "lng": -15.310139
+ }
+ },
+ "GN": {
+ "Mamou Region": {
+ "lat": 10.5736024,
+ "lng": -11.8891721
+ },
+ "Labe Region": {
+ "lat": 11.641159,
+ "lng": -11.8891721
+ },
+ "Conakry Region": {
+ "lat": 9.5090945,
+ "lng": -13.7119312
+ },
+ "Boke Region": {
+ "lat": 11.1864672,
+ "lng": -14.1001326
+ },
+ "Faranah": {
+ "lat": 10.2587018,
+ "lng": -10.8000051
+ },
+ "Kindia": {
+ "lat": 10.0406725,
+ "lng": -12.8629885
+ },
+ "Kankan Region": {
+ "lat": 10.120923,
+ "lng": -9.5450974
+ },
+ "": {
+ "lat": 9.945587,
+ "lng": -9.696645
+ }
+ },
+ "NE": {
+ "Tahoua": {
+ "lat": 14.8904575,
+ "lng": 5.2579968
+ },
+ "Zinder": {
+ "lat": 13.8018124,
+ "lng": 8.98527
+ },
+ "Tillaberi Region": {
+ "lat": 14.6489525,
+ "lng": 2.1450245
+ },
+ "Maradi": {
+ "lat": 13.5009779,
+ "lng": 7.103639599999999
+ },
+ "Niamey": {
+ "lat": 13.5115963,
+ "lng": 2.1253854
+ },
+ "Dosso Region": {
+ "lat": 13.1513947,
+ "lng": 3.4195527
+ },
+ "Diffa": {
+ "lat": 13.3132472,
+ "lng": 12.6158803
+ },
+ "Agadez": {
+ "lat": 16.9741689,
+ "lng": 7.986535
+ },
+ "": {
+ "lat": 17.607789,
+ "lng": 8.081666
+ }
+ },
+ "ML": {
+ "Tombouctou": {
+ "lat": 16.7665887,
+ "lng": -3.0025615
+ },
+ "Gao": {
+ "lat": 16.2639807,
+ "lng": -0.0279867
+ },
+ "Taoudénit": {
+ "lat": 22.6764132,
+ "lng": -3.9789143
+ },
+ "Sikasso": {
+ "lat": 11.3223834,
+ "lng": -5.6983979
+ },
+ "Ségou": {
+ "lat": 13.4316597,
+ "lng": -6.2482149
+ },
+ "Mopti": {
+ "lat": 14.490149,
+ "lng": -4.1924713
+ },
+ "Koulikoro": {
+ "lat": 12.8662046,
+ "lng": -7.5599321
+ },
+ "Kidal": {
+ "lat": 18.4467469,
+ "lng": 1.408939
+ },
+ "Kayes": {
+ "lat": 14.4393538,
+ "lng": -11.4467369
+ },
+ "Bamako Region": {
+ "lat": 12.6392316,
+ "lng": -8.0028892
+ },
+ "": {
+ "lat": 17.570692,
+ "lng": -3.996166
+ }
+ },
+ "MA": {
+ "Laayoune-Sakia El Hamra": {
+ "lat": 27.8683194,
+ "lng": -11.9804613
+ },
+ "Dakhla-Oued Ed-Dahab": {
+ "lat": 31.791702,
+ "lng": -7.092619999999999
+ },
+ "Draa-Tafilalet": {
+ "lat": 31.1499538,
+ "lng": -5.393955099999999
+ },
+ "Marrakesh-Safi": {
+ "lat": 31.7330833,
+ "lng": -8.1338558
+ },
+ "Souss-Massa": {
+ "lat": 30.2750611,
+ "lng": -8.1338558
+ },
+ "Casablanca-Settat": {
+ "lat": 33.0266712,
+ "lng": -7.6114217
+ },
+ "Beni Mellal-Khenifra": {
+ "lat": 32.5719184,
+ "lng": -6.0679194
+ },
+ "Rabat-Sale-Kenitra": {
+ "lat": 34.1727659,
+ "lng": -6.2375947
+ },
+ "Tanger-Tetouan-Al Hoceima": {
+ "lat": 35.2629558,
+ "lng": -5.5617279
+ },
+ "Fes-Meknes": {
+ "lat": 34.062529,
+ "lng": -4.7277528
+ },
+ "Oriental": {
+ "lat": 33.4198879,
+ "lng": -2.1450245
+ },
+ "Guelmim-Oued Noun": {
+ "lat": 28.4844281,
+ "lng": -10.0807298
+ },
+ "": {
+ "lat": 31.791702,
+ "lng": -7.09262
+ }
+ },
+ "TN": {
+ "Zaghouan Governorate": {
+ "lat": 36.2771237,
+ "lng": 9.9912254
+ },
+ "Jendouba Governorate": {
+ "lat": 36.7181862,
+ "lng": 8.748116699999999
+ },
+ "Béja Governorate": {
+ "lat": 36.8625526,
+ "lng": 9.1013498
+ },
+ "Manouba": {
+ "lat": 36.8093284,
+ "lng": 10.0863269
+ },
+ "Gafsa": {
+ "lat": 34.4311398,
+ "lng": 8.7756556
+ },
+ "Tunis Governorate": {
+ "lat": 36.8336172,
+ "lng": 10.2375823
+ },
+ "Tozeur Governorate": {
+ "lat": 33.9789491,
+ "lng": 8.0465185
+ },
+ "Tataouine": {
+ "lat": 32.9210902,
+ "lng": 10.4508956
+ },
+ "Kasserine Governorate": {
+ "lat": 35.0809148,
+ "lng": 8.6600586
+ },
+ "Nabeul Governorate": {
+ "lat": 36.7278251,
+ "lng": 10.7097867
+ },
+ "Kef Governorate": {
+ "lat": 36.1230512,
+ "lng": 8.6600586
+ },
+ "Monastir Governorate": {
+ "lat": 35.564091,
+ "lng": 10.7548851
+ },
+ "Sousse Governorate": {
+ "lat": 35.9022267,
+ "lng": 10.3497895
+ },
+ "Ariana Governorate": {
+ "lat": 36.907169,
+ "lng": 10.1255164
+ },
+ "Siliana Governorate": {
+ "lat": 35.990294,
+ "lng": 9.2785583
+ },
+ "Mahdia Governorate": {
+ "lat": 35.3830893,
+ "lng": 10.3497895
+ },
+ "Sidi Bouzid Governorate": {
+ "lat": 35.0000369,
+ "lng": 9.7232673
+ },
+ "Sfax Governorate": {
+ "lat": 34.685798,
+ "lng": 10.3497895
+ },
+ "Ben Arous Governorate": {
+ "lat": 36.6435606,
+ "lng": 10.2151578
+ },
+ "Kebili Governorate": {
+ "lat": 33.1245286,
+ "lng": 8.8362755
+ },
+ "Medenine Governorate": {
+ "lat": 33.2280565,
+ "lng": 10.8903099
+ },
+ "Gabès Governorate": {
+ "lat": 33.9459648,
+ "lng": 9.7232673
+ },
+ "Bizerte Governorate": {
+ "lat": 37.1609397,
+ "lng": 9.634134999999999
+ },
+ "Kairouan": {
+ "lat": 35.6711663,
+ "lng": 10.1005469
+ },
+ "": {
+ "lat": 33.886917,
+ "lng": 9.537499
+ }
+ },
+ "DZ": {
+ "Boumerdes": {
+ "lat": 36.7590552,
+ "lng": 3.4731462
+ },
+ "Constantine": {
+ "lat": 36.3570052,
+ "lng": 6.639028199999999
+ },
+ "Biskra": {
+ "lat": 34.8536654,
+ "lng": 5.727726199999999
+ },
+ "Algiers": {
+ "lat": 36.753768,
+ "lng": 3.0587561
+ },
+ "Djelfa": {
+ "lat": 34.6703956,
+ "lng": 3.2503761
+ },
+ "Illizi": {
+ "lat": 26.5081447,
+ "lng": 8.483017499999999
+ },
+ "Tizi Ouzou": {
+ "lat": 36.713548,
+ "lng": 4.0473075
+ },
+ "El Tarf": {
+ "lat": 36.7667584,
+ "lng": 8.3167895
+ },
+ "Touggourt": {
+ "lat": 33.1049642,
+ "lng": 6.0662834
+ },
+ "Jijel": {
+ "lat": 36.8210144,
+ "lng": 5.7634126
+ },
+ "Tlemcen": {
+ "lat": 34.8828864,
+ "lng": -1.3166815
+ },
+ "Tissemsilt": {
+ "lat": 35.6053781,
+ "lng": 1.813098
+ },
+ "Tipaza": {
+ "lat": 36.5906719,
+ "lng": 2.4433723
+ },
+ "Tindouf": {
+ "lat": 27.677069,
+ "lng": -8.128375000000002
+ },
+ "Timimoun": {
+ "lat": 29.2616911,
+ "lng": 0.2415964
+ },
+ "Mascara": {
+ "lat": 35.4020988,
+ "lng": 0.1400257
+ },
+ "Tiaret": {
+ "lat": 35.3708689,
+ "lng": 1.3217852
+ },
+ "Chlef": {
+ "lat": 36.1581301,
+ "lng": 1.3371874
+ },
+ "Mila": {
+ "lat": 36.4519049,
+ "lng": 6.2584338
+ },
+ "Tébessa": {
+ "lat": 35.4010797,
+ "lng": 8.1172958
+ },
+ "Batna": {
+ "lat": 35.5446077,
+ "lng": 6.1596945
+ },
+ "Béjaïa": {
+ "lat": 36.7514502,
+ "lng": 5.0557713
+ },
+ "Tamanrasset": {
+ "lat": 22.7942358,
+ "lng": 5.5361426
+ },
+ "Skikda": {
+ "lat": 36.8620804,
+ "lng": 6.9054888
+ },
+ "Medea": {
+ "lat": 36.2636153,
+ "lng": 2.7586773
+ },
+ "Bouira": {
+ "lat": 36.3737098,
+ "lng": 3.8980159
+ },
+ "Blida": {
+ "lat": 36.4735715,
+ "lng": 2.8323153
+ },
+ "Souk Ahras": {
+ "lat": 36.2799517,
+ "lng": 7.9382788
+ },
+ "Relizane": {
+ "lat": 35.7450263,
+ "lng": 0.5578837
+ },
+ "Oran": {
+ "lat": 35.6987388,
+ "lng": -0.6349319
+ },
+ "Aïn Témouchent": {
+ "lat": 35.3038931,
+ "lng": -1.1391863
+ },
+ "Sidi Bel Abbès": {
+ "lat": 35.2105876,
+ "lng": -0.629983
+ },
+ "M'Sila": {
+ "lat": 35.7186646,
+ "lng": 4.523342299999999
+ },
+ "Sétif": {
+ "lat": 36.1897593,
+ "lng": 5.4107984
+ },
+ "Saida": {
+ "lat": 34.8381764,
+ "lng": 0.1481254
+ },
+ "El Oued": {
+ "lat": 33.367811,
+ "lng": 6.8516511
+ },
+ "Adrar": {
+ "lat": 27.8808838,
+ "lng": -0.28943
+ },
+ "Bordj Bou Arréridj": {
+ "lat": 36.0739925,
+ "lng": 4.7630271
+ },
+ "Oum el Bouaghi": {
+ "lat": 35.8688789,
+ "lng": 7.110826599999999
+ },
+ "Ouled Djellal": {
+ "lat": 34.4303855,
+ "lng": 5.0611761
+ },
+ "Guelma": {
+ "lat": 36.4627444,
+ "lng": 7.4330833
+ },
+ "Ouargla": {
+ "lat": 31.9527411,
+ "lng": 5.3335348
+ },
+ "Naama": {
+ "lat": 33.2667317,
+ "lng": -0.3128659
+ },
+ "Mostaganem": {
+ "lat": 35.9311454,
+ "lng": 0.09094139999999999
+ },
+ "Beni Abbes": {
+ "lat": 30.1312217,
+ "lng": -2.1662258
+ },
+ "Aïn Defla": {
+ "lat": 36.2552144,
+ "lng": 1.9562997
+ },
+ "Laghouat": {
+ "lat": 33.8079718,
+ "lng": 2.8628592
+ },
+ "Khenchela": {
+ "lat": 35.4269404,
+ "lng": 7.1460155
+ },
+ "Béchar": {
+ "lat": 31.6238098,
+ "lng": -2.2162443
+ },
+ "Djanet": {
+ "lat": 24.554151,
+ "lng": 9.485429
+ },
+ "In Salah": {
+ "lat": 27.1977907,
+ "lng": 2.4818223
+ },
+ "Ghardaia": {
+ "lat": 32.4943741,
+ "lng": 3.64446
+ },
+ "El Bayadh": {
+ "lat": 33.6854149,
+ "lng": 1.0303543
+ },
+ "In Guezzam": {
+ "lat": 19.5732736,
+ "lng": 5.7693343
+ },
+ "El Mghair": {
+ "lat": 33.950285,
+ "lng": 5.9244238
+ },
+ "Annaba": {
+ "lat": 36.897375,
+ "lng": 7.7500122
+ },
+ "El Menia": {
+ "lat": 30.5833161,
+ "lng": 2.8836701
+ },
+ "Bordj Badji Mokhtar": {
+ "lat": 21.32551,
+ "lng": 0.9524803
+ },
+ "": {
+ "lat": 28.033886,
+ "lng": 1.659626
+ }
+ },
+ "ES": {
+ "Andalusia": {
+ "lat": 37.5442706,
+ "lng": -4.7277528
+ },
+ "Extremadura": {
+ "lat": 39.4937392,
+ "lng": -6.0679194
+ },
+ "Murcia": {
+ "lat": 37.9922399,
+ "lng": -1.1306544
+ },
+ "Castille-La Mancha": {
+ "lat": 39.2795607,
+ "lng": -3.097702
+ },
+ "Valencia": {
+ "lat": 39.4699075,
+ "lng": -0.3762881
+ },
+ "Canary Islands": {
+ "lat": 28.2915637,
+ "lng": -16.6291304
+ },
+ "Balearic Islands": {
+ "lat": 39.35877591790004,
+ "lng": 2.735632782011336
+ },
+ "Melilla": {
+ "lat": 35.2922775,
+ "lng": -2.9380973
+ },
+ "Basque Country": {
+ "lat": 42.9896248,
+ "lng": -2.6189273
+ },
+ "Aragon": {
+ "lat": 41.5976275,
+ "lng": -0.9056623
+ },
+ "Galicia": {
+ "lat": 42.5750554,
+ "lng": -8.1338558
+ },
+ "Madrid": {
+ "lat": 40.4167754,
+ "lng": -3.7037902
+ },
+ "Castille and León": {
+ "lat": 41.83568210000001,
+ "lng": -4.397635699999999
+ },
+ "Cantabria": {
+ "lat": 43.1828396,
+ "lng": -3.9878427
+ },
+ "Catalonia": {
+ "lat": 41.5911589,
+ "lng": 1.5208624
+ },
+ "Principality of Asturias": {
+ "lat": 43.3613953,
+ "lng": -5.8593267
+ },
+ "Navarre": {
+ "lat": 42.6953909,
+ "lng": -1.6760691
+ },
+ "La Rioja": {
+ "lat": 42.2870733,
+ "lng": -2.539603
+ },
+ "Ceuta": {
+ "lat": 35.8893874,
+ "lng": -5.3213455
+ },
+ "": {
+ "lat": 40.463667,
+ "lng": -3.74922
+ }
+ },
+ "IT": {
+ "Calabria": {
+ "lat": 39.3087714,
+ "lng": 16.3463791
+ },
+ "Sicily": {
+ "lat": 37.5999938,
+ "lng": 14.0153557
+ },
+ "Sardinia": {
+ "lat": 40.1208752,
+ "lng": 9.012892599999999
+ },
+ "Basilicate": {
+ "lat": 40.6430766,
+ "lng": 15.9699878
+ },
+ "Apulia": {
+ "lat": 40.7928393,
+ "lng": 17.1011931
+ },
+ "Campania": {
+ "lat": 41.10994729999999,
+ "lng": 14.8475139
+ },
+ "Veneto": {
+ "lat": 45.4414662,
+ "lng": 12.3152595
+ },
+ "Emilia-Romagna": {
+ "lat": 44.5967607,
+ "lng": 11.2186396
+ },
+ "Friuli Venezia Giulia": {
+ "lat": 46.2259177,
+ "lng": 13.1033646
+ },
+ "Lombardy": {
+ "lat": 45.47906709999999,
+ "lng": 9.8452433
+ },
+ "Liguria": {
+ "lat": 44.3167917,
+ "lng": 8.3964938
+ },
+ "Trentino-Alto Adige": {
+ "lat": 46.4336662,
+ "lng": 11.1693296
+ },
+ "Lazio": {
+ "lat": 41.6552418,
+ "lng": 12.989615
+ },
+ "Piedmont": {
+ "lat": 45.0522366,
+ "lng": 7.5153885
+ },
+ "Tuscany": {
+ "lat": 43.7710513,
+ "lng": 11.2486208
+ },
+ "Abruzzo": {
+ "lat": 42.1920119,
+ "lng": 13.7289167
+ },
+ "The Marches": {
+ "lat": 43.5058744,
+ "lng": 12.989615
+ },
+ "Molise": {
+ "lat": 41.67388649999999,
+ "lng": 14.7520939
+ },
+ "Umbria": {
+ "lat": 42.938004,
+ "lng": 12.6216211
+ },
+ "Aosta Valley": {
+ "lat": 45.7388878,
+ "lng": 7.426186599999999
+ },
+ "": {
+ "lat": 41.87194,
+ "lng": 12.56738
+ }
+ },
+ "MT": {
+ "Marsaxlokk": {
+ "lat": 35.8422063,
+ "lng": 14.5427735
+ },
+ "Iz-Zurrieq": {
+ "lat": 35.8302346,
+ "lng": 14.4751779
+ },
+ "Iz-Zejtun": {
+ "lat": 35.8557905,
+ "lng": 14.5332143
+ },
+ "Iz-Zebbug": {
+ "lat": 36.0671351,
+ "lng": 14.234817
+ },
+ "Haz-Zabbar": {
+ "lat": 35.8771148,
+ "lng": 14.540596
+ },
+ "Ix-Xghajra": {
+ "lat": 35.8865072,
+ "lng": 14.5467404
+ },
+ "Ix-Xewkija": {
+ "lat": 36.0321998,
+ "lng": 14.2608044
+ },
+ "Ix-Xaghra": {
+ "lat": 36.0500932,
+ "lng": 14.2643301
+ },
+ "Valletta": {
+ "lat": 35.8992375,
+ "lng": 14.5140996
+ },
+ "Tarxien": {
+ "lat": 35.8668509,
+ "lng": 14.5124736
+ },
+ "Ta' Xbiex": {
+ "lat": 35.898976,
+ "lng": 14.4949271
+ },
+ "Tas-Sliema": {
+ "lat": 35.9123988,
+ "lng": 14.5017677
+ },
+ "Is-Siggiewi": {
+ "lat": 35.8537819,
+ "lng": 14.4385572
+ },
+ "Saint Venera": {
+ "lat": 35.888949,
+ "lng": 14.4780874
+ },
+ "Saint Lucia": {
+ "lat": 35.8628915,
+ "lng": 14.5055214
+ },
+ "Saint Paul’s Bay": {
+ "lat": 35.9483408,
+ "lng": 14.4109413
+ },
+ "Sannat": {
+ "lat": 36.0237371,
+ "lng": 14.2457123
+ },
+ "Saint Lawrence": {
+ "lat": 36.05406,
+ "lng": 14.2026799
+ },
+ "Saint Julian": {
+ "lat": 35.9214241,
+ "lng": 14.4905868
+ },
+ "Safi": {
+ "lat": 35.833088,
+ "lng": 14.4849813
+ },
+ "Paola": {
+ "lat": 35.8729954,
+ "lng": 14.507489
+ },
+ "Victoria": {
+ "lat": 36.0447227,
+ "lng": 14.2409977
+ },
+ "Il-Qrendi": {
+ "lat": 35.8342549,
+ "lng": 14.4577862
+ },
+ "Qormi": {
+ "lat": 35.8785358,
+ "lng": 14.4705233
+ },
+ "Il-Qala": {
+ "lat": 36.0347623,
+ "lng": 14.3095825
+ },
+ "Tal-Pieta": {
+ "lat": 35.8927838,
+ "lng": 14.4934702
+ },
+ "In-Naxxar": {
+ "lat": 35.9145712,
+ "lng": 14.4442551
+ },
+ "In-Nadur": {
+ "lat": 36.0380524,
+ "lng": 14.2929803
+ },
+ "Il-Munxar": {
+ "lat": 36.0315459,
+ "lng": 14.2359164
+ },
+ "L-Imsida": {
+ "lat": 35.8993715,
+ "lng": 14.4845673
+ },
+ "L-Imqabba": {
+ "lat": 35.842057,
+ "lng": 14.4651729
+ },
+ "Il-Mosta": {
+ "lat": 35.9094431,
+ "lng": 14.4258739
+ },
+ "L-Imgarr": {
+ "lat": 35.9198177,
+ "lng": 14.3658099
+ },
+ "Il-Mellieha": {
+ "lat": 35.9565528,
+ "lng": 14.363558
+ },
+ "L-Imdina": {
+ "lat": 35.8863691,
+ "lng": 14.4031146
+ },
+ "Marsaskala": {
+ "lat": 35.86281779999999,
+ "lng": 14.5697064
+ },
+ "Il-Marsa": {
+ "lat": 35.8831345,
+ "lng": 14.4947288
+ },
+ "Is-Swieqi": {
+ "lat": 35.921781,
+ "lng": 14.4788318
+ },
+ "Luqa": {
+ "lat": 35.8598629,
+ "lng": 14.4888722
+ },
+ "Senglea": {
+ "lat": 35.8878874,
+ "lng": 14.5167449
+ },
+ "L-Iklin": {
+ "lat": 35.9060466,
+ "lng": 14.4528477
+ },
+ "Lija": {
+ "lat": 35.9018584,
+ "lng": 14.4480114
+ },
+ "Ta' Kercem": {
+ "lat": 36.0416864,
+ "lng": 14.2270714
+ },
+ "Il-Kalkara": {
+ "lat": 35.8890117,
+ "lng": 14.5309693
+ },
+ "Il-Gzira": {
+ "lat": 35.9049436,
+ "lng": 14.4936571
+ },
+ "Floriana": {
+ "lat": 35.8922208,
+ "lng": 14.5036155
+ },
+ "Il-Birgu": {
+ "lat": 35.8880695,
+ "lng": 14.5220972
+ },
+ "Il-Hamrun": {
+ "lat": 35.8866813,
+ "lng": 14.4875714
+ },
+ "Il-Gudja": {
+ "lat": 35.848248,
+ "lng": 14.5027222
+ },
+ "Hal Ghaxaq": {
+ "lat": 35.847411,
+ "lng": 14.5147543
+ },
+ "L-Ghasri": {
+ "lat": 36.0633571,
+ "lng": 14.2218064
+ },
+ "Hal Gharghur": {
+ "lat": 35.9237367,
+ "lng": 14.453202
+ },
+ "L-Gharb": {
+ "lat": 36.0598645,
+ "lng": 14.2090723
+ },
+ "Ghajnsielem": {
+ "lat": 36.0257218,
+ "lng": 14.28641
+ },
+ "Il-Fgura": {
+ "lat": 35.8723777,
+ "lng": 14.5214089
+ },
+ "Dingli": {
+ "lat": 35.8599678,
+ "lng": 14.3796965
+ },
+ "Bormla": {
+ "lat": 35.882448,
+ "lng": 14.522503
+ },
+ "Birzebbuga": {
+ "lat": 35.8265339,
+ "lng": 14.5278428
+ },
+ "Birkirkara": {
+ "lat": 35.8961327,
+ "lng": 14.4644929
+ },
+ "Balzan": {
+ "lat": 35.8983624,
+ "lng": 14.4521396
+ },
+ "Attard": {
+ "lat": 35.893269,
+ "lng": 14.4368125
+ },
+ "Saint John": {
+ "lat": 35.9079226,
+ "lng": 14.478277
+ },
+ "Il-Fontana": {
+ "lat": 36.0388917,
+ "lng": 14.2369197
+ },
+ "Pembroke": {
+ "lat": 35.9261416,
+ "lng": 14.4803358
+ },
+ "": {
+ "lat": 35.937496,
+ "lng": 14.375416
+ }
+ },
+ "AT": {
+ "Lower Austria": {
+ "lat": 48.10807699999999,
+ "lng": 15.8049558
+ },
+ "Upper Austria": {
+ "lat": 48.025854,
+ "lng": 13.9723665
+ },
+ "Salzburg": {
+ "lat": 47.80949,
+ "lng": 13.05501
+ },
+ "Styria": {
+ "lat": 47.3593442,
+ "lng": 14.4699827
+ },
+ "Carinthia": {
+ "lat": 46.722203,
+ "lng": 14.1805881
+ },
+ "Burgenland": {
+ "lat": 47.15371649999999,
+ "lng": 16.2688797
+ },
+ "Tyrol": {
+ "lat": 47.2537414,
+ "lng": 11.601487
+ },
+ "Vorarlberg": {
+ "lat": 47.2497427,
+ "lng": 9.9797373
+ },
+ "Vienna": {
+ "lat": 48.2081743,
+ "lng": 16.3738189
+ },
+ "": {
+ "lat": 47.516231,
+ "lng": 14.550072
+ }
+ },
+ "DK": {
+ "North Denmark": {
+ "lat": 56.8307416,
+ "lng": 9.4930528
+ },
+ "Zealand": {
+ "lat": 55.4632518,
+ "lng": 11.7214979
+ },
+ "South Denmark": {
+ "lat": 55.3307714,
+ "lng": 9.0924903
+ },
+ "Capital Region": {
+ "lat": 55.8565124,
+ "lng": 12.3011513
+ },
+ "Central Jutland": {
+ "lat": 56.302139,
+ "lng": 9.3027769
+ },
+ "": {
+ "lat": 56.26392,
+ "lng": 9.501785
+ }
+ },
+ "IS": {
+ "East": {
+ "lat": 64.8329974,
+ "lng": -15.7433981
+ },
+ "South": {
+ "lat": 64.2661426,
+ "lng": -18.8158138
+ },
+ "Northwest": {
+ "lat": 65.19625049999999,
+ "lng": -19.2425804
+ },
+ "Northeast": {
+ "lat": 65.4713712,
+ "lng": -17.0280279
+ },
+ "Southern Peninsula": {
+ "lat": 63.91548030000001,
+ "lng": -22.3649667
+ },
+ "Westfjords": {
+ "lat": 65.91961500000001,
+ "lng": -21.8811763
+ },
+ "West": {
+ "lat": 64.963051,
+ "lng": -19.020835
+ },
+ "Capital Region": {
+ "lat": 64.1846369,
+ "lng": -21.6131424
+ },
+ "": {
+ "lat": 64.963051,
+ "lng": -19.020835
+ }
+ },
+ "GB": {
+ "Wales": {
+ "lat": 52.1306607,
+ "lng": -3.7837117
+ },
+ "England": {
+ "lat": 52.3555177,
+ "lng": -1.1743197
+ },
+ "Scotland": {
+ "lat": 56.49067119999999,
+ "lng": -4.2026458
+ },
+ "Northern Ireland": {
+ "lat": 54.7877149,
+ "lng": -6.4923145
+ },
+ "": {
+ "lat": 55.378051,
+ "lng": -3.435973
+ }
+ },
+ "IE": {
+ "Ulster": {
+ "lat": 54.7616555,
+ "lng": -6.9612273
+ },
+ "Connacht": {
+ "lat": 53.83762429999999,
+ "lng": -8.9584481
+ },
+ "Munster": {
+ "lat": 52.2217725,
+ "lng": -8.556663799999999
+ },
+ "Leinster": {
+ "lat": 53.3271538,
+ "lng": -7.514084100000001
+ },
+ "": {
+ "lat": 53.41291,
+ "lng": -8.24389
+ }
+ },
+ "CH": {
+ "Basel-Landschaft": {
+ "lat": 47.44181220000001,
+ "lng": 7.7644002
+ },
+ "Bern": {
+ "lat": 46.9479739,
+ "lng": 7.4474468
+ },
+ "Saint Gallen": {
+ "lat": 47.4244818,
+ "lng": 9.3767173
+ },
+ "Aargau": {
+ "lat": 47.3876664,
+ "lng": 8.2554295
+ },
+ "Zurich": {
+ "lat": 47.3768866,
+ "lng": 8.541694
+ },
+ "Grisons": {
+ "lat": 46.65698709999999,
+ "lng": 9.578025700000001
+ },
+ "Fribourg": {
+ "lat": 46.8064773,
+ "lng": 7.161971899999999
+ },
+ "Zug": {
+ "lat": 47.1661672,
+ "lng": 8.5154946
+ },
+ "Solothurn": {
+ "lat": 47.2088348,
+ "lng": 7.532291
+ },
+ "Valais": {
+ "lat": 46.1904614,
+ "lng": 7.5449226
+ },
+ "Vaud": {
+ "lat": 46.5613135,
+ "lng": 6.536765
+ },
+ "Schwyz": {
+ "lat": 47.0207138,
+ "lng": 8.652988400000002
+ },
+ "Lucerne": {
+ "lat": 47.05016819999999,
+ "lng": 8.3093072
+ },
+ "Nidwalden": {
+ "lat": 46.9267016,
+ "lng": 8.3849982
+ },
+ "Schaffhausen": {
+ "lat": 47.6958897,
+ "lng": 8.6380488
+ },
+ "Thurgau": {
+ "lat": 47.5960787,
+ "lng": 9.1523232
+ },
+ "Appenzell Ausserrhoden": {
+ "lat": 47.366481,
+ "lng": 9.3000916
+ },
+ "Ticino": {
+ "lat": 46.331734,
+ "lng": 8.800452900000002
+ },
+ "Jura": {
+ "lat": 47.3444474,
+ "lng": 7.143060800000001
+ },
+ "Geneva": {
+ "lat": 46.2043907,
+ "lng": 6.1431577
+ },
+ "Uri": {
+ "lat": 46.7738629,
+ "lng": 8.602515300000002
+ },
+ "Neuchâtel": {
+ "lat": 46.9899874,
+ "lng": 6.9292732
+ },
+ "Obwalden": {
+ "lat": 46.877858,
+ "lng": 8.251249
+ },
+ "Glarus": {
+ "lat": 47.0411232,
+ "lng": 9.0679
+ },
+ "Appenzell Innerrhoden": {
+ "lat": 47.3161925,
+ "lng": 9.4316573
+ },
+ "Basel-City": {
+ "lat": 47.5619253,
+ "lng": 7.592767999999999
+ },
+ "": {
+ "lat": 46.818188,
+ "lng": 8.227512
+ }
+ },
+ "SJ": {
+ "Svalbard": {
+ "lat": 71.031818,
+ "lng": -8.2920347
+ },
+ "Jan Mayen": {
+ "lat": 71.031818,
+ "lng": -8.2920347
+ },
+ "": {
+ "lat": 77.553604,
+ "lng": 23.670272
+ }
+ },
+ "NL": {
+ "Overijssel": {
+ "lat": 52.4387814,
+ "lng": 6.5016411
+ },
+ "Gelderland": {
+ "lat": 52.045155,
+ "lng": 5.871823399999999
+ },
+ "Drenthe": {
+ "lat": 52.9476012,
+ "lng": 6.623058599999999
+ },
+ "South Holland": {
+ "lat": 52.0207975,
+ "lng": 4.4937836
+ },
+ "North Holland": {
+ "lat": 52.5205869,
+ "lng": 4.788474
+ },
+ "Friesland": {
+ "lat": 53.1641642,
+ "lng": 5.7817542
+ },
+ "North Brabant": {
+ "lat": 51.4826537,
+ "lng": 5.2321687
+ },
+ "Groningen": {
+ "lat": 53.2193835,
+ "lng": 6.566501799999999
+ },
+ "Zeeland": {
+ "lat": 51.4940309,
+ "lng": 3.8496815
+ },
+ "Utrecht": {
+ "lat": 52.09073739999999,
+ "lng": 5.1214201
+ },
+ "Flevoland": {
+ "lat": 52.5279781,
+ "lng": 5.595350799999999
+ },
+ "Limburg": {
+ "lat": 51.4427238,
+ "lng": 6.0608726
+ },
+ "": {
+ "lat": 52.132633,
+ "lng": 5.291266
+ }
+ },
+ "BE": {
+ "Flanders": {
+ "lat": 51.0950244,
+ "lng": 4.4477809
+ },
+ "Wallonia": {
+ "lat": 50.400501,
+ "lng": 5.1335125
+ },
+ "Brussels Capital": {
+ "lat": 50.8476424,
+ "lng": 4.3571696
+ },
+ "": {
+ "lat": 50.503887,
+ "lng": 4.469936
+ }
+ },
+ "DE": {
+ "Saxony": {
+ "lat": 51.1045407,
+ "lng": 13.2017384
+ },
+ "Hesse": {
+ "lat": 50.6520515,
+ "lng": 9.162437599999999
+ },
+ "Bavaria": {
+ "lat": 48.7904472,
+ "lng": 11.4978895
+ },
+ "Baden-Wurttemberg": {
+ "lat": 48.6616037,
+ "lng": 9.3501336
+ },
+ "Rheinland-Pfalz": {
+ "lat": 50.118346,
+ "lng": 7.3089527
+ },
+ "North Rhine-Westphalia": {
+ "lat": 51.43323669999999,
+ "lng": 7.661593799999999
+ },
+ "Brandenburg": {
+ "lat": 52.4125287,
+ "lng": 12.5316444
+ },
+ "Saxony-Anhalt": {
+ "lat": 51.9502649,
+ "lng": 11.6922735
+ },
+ "Mecklenburg-Vorpommern": {
+ "lat": 53.6126505,
+ "lng": 12.4295953
+ },
+ "Lower Saxony": {
+ "lat": 52.6367036,
+ "lng": 9.8450765
+ },
+ "Thuringia": {
+ "lat": 51.0109892,
+ "lng": 10.845346
+ },
+ "Land Berlin": {
+ "lat": 52.52000659999999,
+ "lng": 13.404954
+ },
+ "Schleswig-Holstein": {
+ "lat": 54.21936720000001,
+ "lng": 9.696116700000001
+ },
+ "Saarland": {
+ "lat": 49.3964234,
+ "lng": 7.0229607
+ },
+ "Bremen": {
+ "lat": 53.07929619999999,
+ "lng": 8.8016937
+ },
+ "Hamburg": {
+ "lat": 53.5488282,
+ "lng": 9.987170299999999
+ },
+ "": {
+ "lat": 51.165691,
+ "lng": 10.451526
+ }
+ },
+ "LU": {
+ "Wiltz": {
+ "lat": 49.96622,
+ "lng": 5.932430600000001
+ },
+ "Clervaux": {
+ "lat": 50.0534803,
+ "lng": 6.0291558
+ },
+ "Grevenmacher": {
+ "lat": 49.680841,
+ "lng": 6.440759300000001
+ },
+ "Luxembourg": {
+ "lat": 49.815273,
+ "lng": 6.129582999999999
+ },
+ "Vianden": {
+ "lat": 49.93397479999999,
+ "lng": 6.2076994
+ },
+ "Esch-sur-Alzette": {
+ "lat": 49.5024342,
+ "lng": 5.9722212
+ },
+ "Capellen": {
+ "lat": 49.646245,
+ "lng": 5.990757599999999
+ },
+ "Diekirch": {
+ "lat": 49.8671784,
+ "lng": 6.159563299999999
+ },
+ "Remich": {
+ "lat": 49.54501699999999,
+ "lng": 6.367422200000001
+ },
+ "Echternach": {
+ "lat": 49.811342,
+ "lng": 6.4175245
+ },
+ "Mersch": {
+ "lat": 49.7481135,
+ "lng": 6.103932899999999
+ },
+ "Redange": {
+ "lat": 49.7640865,
+ "lng": 5.888528099999999
+ },
+ "": {
+ "lat": 49.815273,
+ "lng": 6.129583
+ }
+ },
+ "FR": {
+ "Nouvelle-Aquitaine": {
+ "lat": 45.5990651,
+ "lng": 0.6142169
+ },
+ "Hauts-de-France": {
+ "lat": 49.66361269999999,
+ "lng": 2.5280732
+ },
+ "Grand Est": {
+ "lat": 48.9131152,
+ "lng": 5.4425501
+ },
+ "Corsica": {
+ "lat": 42.0396042,
+ "lng": 9.012892599999999
+ },
+ "Centre-Val de Loire": {
+ "lat": 47.7515686,
+ "lng": 1.6750631
+ },
+ "Auvergne-Rhone-Alpes": {
+ "lat": 45.5126545,
+ "lng": 4.4904519
+ },
+ "Pays de la Loire": {
+ "lat": 47.7632836,
+ "lng": -0.3299687
+ },
+ "Brittany": {
+ "lat": 48.2020471,
+ "lng": -2.9326435
+ },
+ "Normandy": {
+ "lat": 48.87987039999999,
+ "lng": 0.1712529
+ },
+ "Île-de-France": {
+ "lat": 48.8499198,
+ "lng": 2.6370411
+ },
+ "Bourgogne-Franche-Comte": {
+ "lat": 47.02321569999999,
+ "lng": 5.0922632
+ },
+ "Provence-Alpes-Côte d'Azur": {
+ "lat": 43.9351691,
+ "lng": 6.0679194
+ },
+ "Occitanie": {
+ "lat": 43.4636856,
+ "lng": 2.1450245
+ },
+ "": {
+ "lat": 46.227638,
+ "lng": 2.213749
+ }
+ },
+ "AD": {
+ "Sant Julià de Loria": {
+ "lat": 42.4657861,
+ "lng": 1.4921277
+ },
+ "Andorra la Vella": {
+ "lat": 42.50631740000001,
+ "lng": 1.5218355
+ },
+ "Encamp": {
+ "lat": 42.5194138,
+ "lng": 1.6566377
+ },
+ "Ordino": {
+ "lat": 42.5994433,
+ "lng": 1.5402327
+ },
+ "Escaldes-Engordany": {
+ "lat": 42.5100804,
+ "lng": 1.5387862
+ },
+ "La Massana": {
+ "lat": 42.545625,
+ "lng": 1.5147392
+ },
+ "Canillo": {
+ "lat": 42.5666535,
+ "lng": 1.5994581
+ },
+ "": {
+ "lat": 42.546245,
+ "lng": 1.601554
+ }
+ },
+ "LI": {
+ "Vaduz": {
+ "lat": 47.1410303,
+ "lng": 9.5209277
+ },
+ "Triesenberg": {
+ "lat": 47.1224511,
+ "lng": 9.5701985
+ },
+ "Triesen": {
+ "lat": 47.1097988,
+ "lng": 9.5248296
+ },
+ "Schellenberg": {
+ "lat": 47.230966,
+ "lng": 9.546784299999999
+ },
+ "Mauren": {
+ "lat": 47.2189285,
+ "lng": 9.541735
+ },
+ "Schaan": {
+ "lat": 47.1667317,
+ "lng": 9.5112002
+ },
+ "Ruggell": {
+ "lat": 47.24167,
+ "lng": 9.5253371
+ },
+ "Planken": {
+ "lat": 47.1797118,
+ "lng": 9.5618123
+ },
+ "Gamprin": {
+ "lat": 47.213249,
+ "lng": 9.5025195
+ },
+ "Eschen": {
+ "lat": 47.2050219,
+ "lng": 9.5240771
+ },
+ "Balzers": {
+ "lat": 47.0655826,
+ "lng": 9.507452599999999
+ },
+ "": {
+ "lat": 47.166,
+ "lng": 9.555373
+ }
+ },
+ "CZ": {
+ "Kralovehradecky kraj": {
+ "lat": 50.3512484,
+ "lng": 15.7976459
+ },
+ "Central Bohemia": {
+ "lat": 49.81749199999999,
+ "lng": 15.472962
+ },
+ "Olomoucky kraj": {
+ "lat": 49.65865489999999,
+ "lng": 17.0811406
+ },
+ "Zlín": {
+ "lat": 49.2244365,
+ "lng": 17.6627635
+ },
+ "South Moravian": {
+ "lat": 48.9544528,
+ "lng": 16.7676899
+ },
+ "Karlovarsky kraj": {
+ "lat": 50.1435,
+ "lng": 12.7501899
+ },
+ "Jihocesky kraj": {
+ "lat": 48.9457789,
+ "lng": 14.4416055
+ },
+ "Ustecky kraj": {
+ "lat": 50.6119037,
+ "lng": 13.7870086
+ },
+ "Kraj Vysocina": {
+ "lat": 49.44900519999999,
+ "lng": 15.6405934
+ },
+ "Plzeň Region": {
+ "lat": 49.4134812,
+ "lng": 13.3157246
+ },
+ "Moravskoslezsky kraj": {
+ "lat": 49.7305327,
+ "lng": 18.2332637
+ },
+ "Liberecky kraj": {
+ "lat": 50.76627999999999,
+ "lng": 15.0543387
+ },
+ "Prague": {
+ "lat": 50.0755381,
+ "lng": 14.4378005
+ },
+ "Pardubicky kraj": {
+ "lat": 49.9444479,
+ "lng": 16.2856916
+ },
+ "": {
+ "lat": 49.817492,
+ "lng": 15.472962
+ }
+ },
+ "SM": {
+ "Serravalle": {
+ "lat": 43.9690367,
+ "lng": 12.4774099
+ },
+ "Castello di San Marino Citta": {
+ "lat": 43.9355907,
+ "lng": 12.4472806
+ },
+ "Chiesanuova": {
+ "lat": 43.91429050000001,
+ "lng": 12.4208642
+ },
+ "Castello di Montegiardino": {
+ "lat": 43.9052999,
+ "lng": 12.4810542
+ },
+ "Castello di Faetano": {
+ "lat": 43.9348967,
+ "lng": 12.4896554
+ },
+ "Castello di Borgo Maggiore": {
+ "lat": 43.9574882,
+ "lng": 12.4552546
+ },
+ "Castello di Acquaviva": {
+ "lat": 43.945153,
+ "lng": 12.4179988
+ },
+ "": {
+ "lat": 43.94236,
+ "lng": 12.457777
+ }
+ },
+ "HR": {
+ "Split-Dalmatia": {
+ "lat": 43.5240328,
+ "lng": 16.8178377
+ },
+ "Vukovar-Sirmium": {
+ "lat": 45.3452377,
+ "lng": 19.0010204
+ },
+ "Dubrovnik-Neretva": {
+ "lat": 43.0766588,
+ "lng": 17.5268471
+ },
+ "Istria": {
+ "lat": 45.2745018,
+ "lng": 13.8901858
+ },
+ "County of Krapina-Zagorje": {
+ "lat": 46.10133930000001,
+ "lng": 15.8809693
+ },
+ "County of Zagreb": {
+ "lat": 45.8706612,
+ "lng": 16.395491
+ },
+ "Karlovac": {
+ "lat": 45.4928973,
+ "lng": 15.5552683
+ },
+ "City of Zagreb": {
+ "lat": 45.8150108,
+ "lng": 15.981919
+ },
+ "County of Zadar": {
+ "lat": 44.1318273,
+ "lng": 15.4556962
+ },
+ "County of Koprivnica-Križevci": {
+ "lat": 46.1568919,
+ "lng": 16.8390826
+ },
+ "County of Sisak-Moslavina": {
+ "lat": 45.4850767,
+ "lng": 16.3731156
+ },
+ "County of Primorje-Gorski Kotar": {
+ "lat": 45.31739959999999,
+ "lng": 14.8167466
+ },
+ "County of Međimurje": {
+ "lat": 46.3766644,
+ "lng": 16.4213298
+ },
+ "Šibenik-Knin": {
+ "lat": 43.9281485,
+ "lng": 16.1037694
+ },
+ "County of Virovitica-Podravina": {
+ "lat": 45.6557985,
+ "lng": 17.7932472
+ },
+ "County of Osijek-Baranja": {
+ "lat": 45.5576428,
+ "lng": 18.3942141
+ },
+ "County of Varaždin": {
+ "lat": 46.23174729999999,
+ "lng": 16.3360558
+ },
+ "Bjelovar-Bilogora": {
+ "lat": 45.7809992,
+ "lng": 16.9936575
+ },
+ "County of Požega-Slavonia": {
+ "lat": 45.3417868,
+ "lng": 17.8114359
+ },
+ "County of Lika-Senj": {
+ "lat": 44.6192218,
+ "lng": 15.4701608
+ },
+ "Brod-Posavina": {
+ "lat": 45.2637951,
+ "lng": 17.3264562
+ },
+ "": {
+ "lat": 45.1,
+ "lng": 15.2
+ }
+ },
+ "BA": {
+ "Republika Srpska": {
+ "lat": 44.7280186,
+ "lng": 17.3148136
+ },
+ "Federation of B&H": {
+ "lat": 43.8562586,
+ "lng": 18.4130763
+ },
+ "Brčko": {
+ "lat": 44.8726563,
+ "lng": 18.8106276
+ },
+ "": {
+ "lat": 43.915886,
+ "lng": 17.679076
+ }
+ },
+ "SI": {
+ "Obcina Zuzemberk": {
+ "lat": 45.830669,
+ "lng": 14.9298228
+ },
+ "Obcina Zirovnica": {
+ "lat": 46.4044239,
+ "lng": 14.1375549
+ },
+ "Obcina Ziri": {
+ "lat": 46.0513094,
+ "lng": 14.111887
+ },
+ "Obcina Lasko": {
+ "lat": 46.1542793,
+ "lng": 15.2359978
+ },
+ "Videm": {
+ "lat": 45.8490426,
+ "lng": 14.6947037
+ },
+ "Jezersko": {
+ "lat": 46.3942794,
+ "lng": 14.4985559
+ },
+ "Gorje": {
+ "lat": 46.3802458,
+ "lng": 14.0685339
+ },
+ "Sveta Trojica v Slovenskih Goricah": {
+ "lat": 46.5771983,
+ "lng": 15.8789679
+ },
+ "Slovenska Bistrica": {
+ "lat": 46.3919813,
+ "lng": 15.5727868
+ },
+ "Kungota": {
+ "lat": 46.6418793,
+ "lng": 15.6036288
+ },
+ "Kranj": {
+ "lat": 46.2428344,
+ "lng": 14.3555417
+ },
+ "Obcina Zetale": {
+ "lat": 46.2784002,
+ "lng": 15.8122225
+ },
+ "Obcina Zelezniki": {
+ "lat": 46.22544,
+ "lng": 14.1692478
+ },
+ "Krsko": {
+ "lat": 45.95899780000001,
+ "lng": 15.4921312
+ },
+ "Obcina Zavrc": {
+ "lat": 46.3856393,
+ "lng": 16.0470768
+ },
+ "Obcina Zalec": {
+ "lat": 46.251984,
+ "lng": 15.1650282
+ },
+ "Obcina Ivancna Gorica": {
+ "lat": 45.9395841,
+ "lng": 14.8047626
+ },
+ "Zagorje ob Savi": {
+ "lat": 46.1345186,
+ "lng": 14.9945975
+ },
+ "Vuzenica": {
+ "lat": 46.5980836,
+ "lng": 15.1657237
+ },
+ "Horjul": {
+ "lat": 46.02253779999999,
+ "lng": 14.2986269
+ },
+ "Vrhnika": {
+ "lat": 45.966583,
+ "lng": 14.2973873
+ },
+ "Obcina Divaca": {
+ "lat": 45.6806069,
+ "lng": 13.9720312
+ },
+ "Vransko": {
+ "lat": 46.2444682,
+ "lng": 14.9511982
+ },
+ "Obcina Rence-Vogrsko": {
+ "lat": 45.8954617,
+ "lng": 13.6785673
+ },
+ "Vojnik": {
+ "lat": 46.2920581,
+ "lng": 15.302058
+ },
+ "Vodice": {
+ "lat": 46.1896643,
+ "lng": 14.493854
+ },
+ "Obcina Ajdovscina": {
+ "lat": 45.8870776,
+ "lng": 13.9042818
+ },
+ "Obcina Sveti Andraz v Slovenskih Goricah": {
+ "lat": 46.5189747,
+ "lng": 15.9498262
+ },
+ "Vitanje": {
+ "lat": 46.3815074,
+ "lng": 15.2950333
+ },
+ "Vipava": {
+ "lat": 45.8455744,
+ "lng": 13.9625431
+ },
+ "Obcina Smarjeske Toplice": {
+ "lat": 45.8849392,
+ "lng": 15.2499203
+ },
+ "Obcina Verzej": {
+ "lat": 46.5832778,
+ "lng": 16.1640173
+ },
+ "Obcina Sentilj": {
+ "lat": 46.6862839,
+ "lng": 15.7103566
+ },
+ "Trebnje": {
+ "lat": 45.9081708,
+ "lng": 15.0125985
+ },
+ "Obcina Velike Lasce": {
+ "lat": 45.8336591,
+ "lng": 14.6362363
+ },
+ "Velika Polana": {
+ "lat": 46.5731715,
+ "lng": 16.3444126
+ },
+ "Obcina Ormoz": {
+ "lat": 46.4353333,
+ "lng": 16.154374
+ },
+ "Grosuplje": {
+ "lat": 45.9557645,
+ "lng": 14.658899
+ },
+ "Mestna Obcina Novo mesto": {
+ "lat": 45.7869649,
+ "lng": 15.1992167
+ },
+ "Obcina Turnisce": {
+ "lat": 46.625456,
+ "lng": 16.3144961
+ },
+ "Sevnica": {
+ "lat": 46.0129508,
+ "lng": 15.2979868
+ },
+ "Trzin": {
+ "lat": 46.1314634,
+ "lng": 14.56125
+ },
+ "Obcina Trzic": {
+ "lat": 46.3593514,
+ "lng": 14.3006623
+ },
+ "Obcina Trnovska vas": {
+ "lat": 46.5201168,
+ "lng": 15.8866431
+ },
+ "Sentjur": {
+ "lat": 46.2157774,
+ "lng": 15.3945807
+ },
+ "Mokronog-Trebelno": {
+ "lat": 45.9088529,
+ "lng": 15.1596736
+ },
+ "Trbovlje": {
+ "lat": 46.1503563,
+ "lng": 15.0453138
+ },
+ "Dravograd": {
+ "lat": 46.589219,
+ "lng": 15.0246021
+ },
+ "Obcina Sveti Tomaz": {
+ "lat": 46.4835846,
+ "lng": 16.0801157
+ },
+ "Obcina Tolmin": {
+ "lat": 46.1857188,
+ "lng": 13.7319838
+ },
+ "Velenje": {
+ "lat": 46.3622743,
+ "lng": 15.1106582
+ },
+ "Celje": {
+ "lat": 46.23974949999999,
+ "lng": 15.2677063
+ },
+ "Tabor": {
+ "lat": 46.2107921,
+ "lng": 15.0174249
+ },
+ "Sveta Ana": {
+ "lat": 46.6483424,
+ "lng": 15.8425218
+ },
+ "Postojna": {
+ "lat": 45.7749704,
+ "lng": 14.2133245
+ },
+ "Obcina Zrece": {
+ "lat": 46.3696591,
+ "lng": 15.3918526
+ },
+ "Obcina Store": {
+ "lat": 46.2205619,
+ "lng": 15.3154297
+ },
+ "Obcina Starse": {
+ "lat": 46.4637374,
+ "lng": 15.7482186
+ },
+ "Obcina Loska Dolina": {
+ "lat": 45.6477908,
+ "lng": 14.4973147
+ },
+ "Obcina Crnomelj": {
+ "lat": 45.5361225,
+ "lng": 15.1944143
+ },
+ "Obcina Kocevje": {
+ "lat": 45.6409009,
+ "lng": 14.8633128
+ },
+ "Komen": {
+ "lat": 45.81752350000001,
+ "lng": 13.7482711
+ },
+ "Kamnik": {
+ "lat": 46.2221964,
+ "lng": 14.6072968
+ },
+ "Obcina Bovec": {
+ "lat": 46.3380495,
+ "lng": 13.5524174
+ },
+ "Obcina Sredisce ob Dravi": {
+ "lat": 46.4061386,
+ "lng": 16.2536567
+ },
+ "Gornja Radgona": {
+ "lat": 46.6767099,
+ "lng": 15.9910846
+ },
+ "Duplek": {
+ "lat": 46.5010016,
+ "lng": 15.7546307
+ },
+ "Obcina Domzale": {
+ "lat": 46.1438269,
+ "lng": 14.6375279
+ },
+ "Lenart": {
+ "lat": 46.5834424,
+ "lng": 15.8262124
+ },
+ "Idrija": {
+ "lat": 46.00294539999999,
+ "lng": 14.0278459
+ },
+ "Hajdina": {
+ "lat": 46.4185014,
+ "lng": 15.8244722
+ },
+ "Obcina Sostanj": {
+ "lat": 46.3782836,
+ "lng": 15.0461378
+ },
+ "Obcina Solcava": {
+ "lat": 46.4023526,
+ "lng": 14.6802304
+ },
+ "Obcina Sodrazica": {
+ "lat": 45.762125,
+ "lng": 14.6362158
+ },
+ "Medvode": {
+ "lat": 46.1413926,
+ "lng": 14.411347
+ },
+ "Slovenj Gradec": {
+ "lat": 46.5075851,
+ "lng": 15.0768162
+ },
+ "Obcina Smartno pri Litiji": {
+ "lat": 46.0457437,
+ "lng": 14.8410058
+ },
+ "Obcina Smartno ob Paki": {
+ "lat": 46.3416137,
+ "lng": 15.0277226
+ },
+ "Nazarje": {
+ "lat": 46.3179494,
+ "lng": 14.9470385
+ },
+ "Obcina Sezana": {
+ "lat": 45.7275109,
+ "lng": 13.8661931
+ },
+ "Obcina Smarje pri Jelsah": {
+ "lat": 46.2287025,
+ "lng": 15.5190353
+ },
+ "Slovenske Konjice": {
+ "lat": 46.3369191,
+ "lng": 15.4214708
+ },
+ "Obcina Hoce-Slivnica": {
+ "lat": 46.477858,
+ "lng": 15.6476005
+ },
+ "Ig": {
+ "lat": 45.95888679999999,
+ "lng": 14.5270528
+ },
+ "Obcina Skofljica": {
+ "lat": 45.9840962,
+ "lng": 14.5746626
+ },
+ "Škofja Loka": {
+ "lat": 46.1382331,
+ "lng": 14.148966
+ },
+ "Koper": {
+ "lat": 45.54805899999999,
+ "lng": 13.7301877
+ },
+ "Obcina Sentrupert": {
+ "lat": 45.9769612,
+ "lng": 15.0909889
+ },
+ "Obcina Sentjernej": {
+ "lat": 45.843413,
+ "lng": 15.3378312
+ },
+ "Obcina Sencur": {
+ "lat": 46.2433699,
+ "lng": 14.4192223
+ },
+ "Obcina Sempeter-Vrtojba": {
+ "lat": 45.9290095,
+ "lng": 13.6415594
+ },
+ "Nova Gorica": {
+ "lat": 45.9549755,
+ "lng": 13.6493044
+ },
+ "Obcina Semic": {
+ "lat": 45.6520534,
+ "lng": 15.1820701
+ },
+ "Piran": {
+ "lat": 45.528319,
+ "lng": 13.5682895
+ },
+ "Obcina Salovci": {
+ "lat": 46.8533568,
+ "lng": 16.2591791
+ },
+ "Obcina Ruse": {
+ "lat": 46.5206265,
+ "lng": 15.4817869
+ },
+ "Rogatec": {
+ "lat": 46.2258891,
+ "lng": 15.7000313
+ },
+ "Obcina Rogasovci": {
+ "lat": 46.8083883,
+ "lng": 16.0339763
+ },
+ "Obcina Rogaska Slatina": {
+ "lat": 46.2453973,
+ "lng": 15.6265014
+ },
+ "Obcina Kanal ob Soci": {
+ "lat": 46.067353,
+ "lng": 13.620335
+ },
+ "Ribnica na Pohorju": {
+ "lat": 46.5356403,
+ "lng": 15.2675136
+ },
+ "Ribnica": {
+ "lat": 45.7400303,
+ "lng": 14.7265782
+ },
+ "Pivka": {
+ "lat": 45.6825301,
+ "lng": 14.1960582
+ },
+ "Obcina Recica ob Savinji": {
+ "lat": 46.323379,
+ "lng": 14.922367
+ },
+ "Obcina Ravne na Koroskem": {
+ "lat": 46.5448323,
+ "lng": 14.9660445
+ },
+ "Cerknica": {
+ "lat": 45.7955099,
+ "lng": 14.3621843
+ },
+ "Obcina Luce": {
+ "lat": 46.35487209999999,
+ "lng": 14.7458015
+ },
+ "Radovljica": {
+ "lat": 46.3437361,
+ "lng": 14.1740042
+ },
+ "Radlje ob Dravi": {
+ "lat": 46.614358,
+ "lng": 15.2255472
+ },
+ "Radenci": {
+ "lat": 46.6439196,
+ "lng": 16.0402676
+ },
+ "Obcina Radece": {
+ "lat": 46.0666954,
+ "lng": 15.1820438
+ },
+ "Obcina Race-Fram": {
+ "lat": 46.4542083,
+ "lng": 15.6329467
+ },
+ "Puconci": {
+ "lat": 46.7046395,
+ "lng": 16.1572755
+ },
+ "Obcina Majsperk": {
+ "lat": 46.3503019,
+ "lng": 15.7340595
+ },
+ "Ptuj": {
+ "lat": 46.4199535,
+ "lng": 15.8696884
+ },
+ "Obcina Podcetrtek": {
+ "lat": 46.1739542,
+ "lng": 15.6013816
+ },
+ "Prevalje": {
+ "lat": 46.54687879999999,
+ "lng": 14.9197479
+ },
+ "Brezovica": {
+ "lat": 45.9559351,
+ "lng": 14.4349952
+ },
+ "Ilirska Bistrica": {
+ "lat": 45.570099,
+ "lng": 14.2418616
+ },
+ "Preddvor": {
+ "lat": 46.3017155,
+ "lng": 14.4216753
+ },
+ "Prebold": {
+ "lat": 46.2365972,
+ "lng": 15.091648
+ },
+ "Polzela": {
+ "lat": 46.280897,
+ "lng": 15.0737321
+ },
+ "Litija": {
+ "lat": 46.05923050000001,
+ "lng": 14.8266015
+ },
+ "Obcina Poljcane": {
+ "lat": 46.3141609,
+ "lng": 15.5784672
+ },
+ "Gorenja Vas-Poljane": {
+ "lat": 46.1116582,
+ "lng": 14.1149347
+ },
+ "Dobrova-Polhov Gradec": {
+ "lat": 46.0648896,
+ "lng": 14.3168195
+ },
+ "Podvelka": {
+ "lat": 46.5903517,
+ "lng": 15.3310343
+ },
+ "Kozje": {
+ "lat": 46.0733211,
+ "lng": 15.5596719
+ },
+ "Naklo": {
+ "lat": 46.2718663,
+ "lng": 14.3156932
+ },
+ "Podlehnik": {
+ "lat": 46.3358974,
+ "lng": 15.8787245
+ },
+ "Jesenice": {
+ "lat": 46.4367047,
+ "lng": 14.0526057
+ },
+ "Pesnica": {
+ "lat": 46.6373877,
+ "lng": 15.5543464
+ },
+ "Muta": {
+ "lat": 46.62720059999999,
+ "lng": 15.1348663
+ },
+ "Osilnica": {
+ "lat": 45.5291637,
+ "lng": 14.6984205
+ },
+ "Oplotnica": {
+ "lat": 46.387163,
+ "lng": 15.4458131
+ },
+ "Odranci": {
+ "lat": 46.5847279,
+ "lng": 16.2762351
+ },
+ "Murska Sobota": {
+ "lat": 46.6581381,
+ "lng": 16.1610293
+ },
+ "Mozirje": {
+ "lat": 46.3392772,
+ "lng": 14.9599807
+ },
+ "Moravske Toplice": {
+ "lat": 46.6856932,
+ "lng": 16.2224582
+ },
+ "Obcina Moravce": {
+ "lat": 46.1357218,
+ "lng": 14.7444352
+ },
+ "Kranjska Gora": {
+ "lat": 46.4845293,
+ "lng": 13.7857145
+ },
+ "Mislinja": {
+ "lat": 46.4439734,
+ "lng": 15.1985947
+ },
+ "Obcina Mirna Pec": {
+ "lat": 45.8481574,
+ "lng": 15.087945
+ },
+ "Mirna": {
+ "lat": 45.97311560728623,
+ "lng": 15.19467448424267
+ },
+ "Miren-Kostanjevica": {
+ "lat": 45.84360290000001,
+ "lng": 13.6276647
+ },
+ "Obcina Miklavz na Dravskem Polju": {
+ "lat": 46.4836246,
+ "lng": 15.7119067
+ },
+ "Obcina Mezica": {
+ "lat": 46.5214145,
+ "lng": 14.8523952
+ },
+ "Metlika": {
+ "lat": 45.6473994,
+ "lng": 15.3176752
+ },
+ "Obcina Menges": {
+ "lat": 46.16591220000001,
+ "lng": 14.5719694
+ },
+ "Hrpelje-Kozina": {
+ "lat": 45.60911919999999,
+ "lng": 13.9379148
+ },
+ "Maribor": {
+ "lat": 46.5546503,
+ "lng": 15.6458812
+ },
+ "Makole": {
+ "lat": 46.3173644,
+ "lng": 15.6670441
+ },
+ "Lukovica": {
+ "lat": 46.1696293,
+ "lng": 14.6907259
+ },
+ "Lovrenc na Pohorju": {
+ "lat": 46.54027749999999,
+ "lng": 15.3884817
+ },
+ "Obcina Loski Potok": {
+ "lat": 45.6909637,
+ "lng": 14.5985971
+ },
+ "Log–Dragomer": {
+ "lat": 46.0178747,
+ "lng": 14.3687767
+ },
+ "Ljutomer": {
+ "lat": 46.5193583,
+ "lng": 16.197814
+ },
+ "Ljubljana": {
+ "lat": 46.0569465,
+ "lng": 14.5057515
+ },
+ "Lendava": {
+ "lat": 46.5644783,
+ "lng": 16.453063
+ },
+ "Kuzma": {
+ "lat": 46.8374998,
+ "lng": 16.0993411
+ },
+ "Obcina Krizevci": {
+ "lat": 46.5701821,
+ "lng": 16.1092653
+ },
+ "Kostanjevica na Krki": {
+ "lat": 45.8449724,
+ "lng": 15.4217164
+ },
+ "Kobilje": {
+ "lat": 46.68518,
+ "lng": 16.3936719
+ },
+ "Obcina Kobarid": {
+ "lat": 46.2476549,
+ "lng": 13.5791749
+ },
+ "Obcina Kidricevo": {
+ "lat": 46.4050135,
+ "lng": 15.7947234
+ },
+ "Obcina Jursinci": {
+ "lat": 46.4850899,
+ "lng": 15.9714564
+ },
+ "Sveti Jurij v Slovenskih Goricah": {
+ "lat": 46.6170791,
+ "lng": 15.7804677
+ },
+ "Obcina Sveti Jurij ob Scavnici": {
+ "lat": 46.5687452,
+ "lng": 16.0222528
+ },
+ "Izola": {
+ "lat": 45.5374048,
+ "lng": 13.6600802
+ },
+ "Hrastnik": {
+ "lat": 46.1417288,
+ "lng": 15.0844894
+ },
+ "Logatec": {
+ "lat": 45.917611,
+ "lng": 14.2351451
+ },
+ "Hodos": {
+ "lat": 46.8314134,
+ "lng": 16.321068
+ },
+ "Grad": {
+ "lat": 46.808732,
+ "lng": 16.109206
+ },
+ "Gornji Petrovci": {
+ "lat": 46.8047535,
+ "lng": 16.2180722
+ },
+ "Gornji Grad": {
+ "lat": 46.2961712,
+ "lng": 14.8062347
+ },
+ "Obcina Gorisnica": {
+ "lat": 46.4120271,
+ "lng": 16.0133089
+ },
+ "Obcina Straza": {
+ "lat": 45.7768428,
+ "lng": 15.0948694
+ },
+ "Obcina Braslovce": {
+ "lat": 46.2836192,
+ "lng": 15.0418321
+ },
+ "Obcina Brezice": {
+ "lat": 45.9041096,
+ "lng": 15.5943639
+ },
+ "Obcina Tisina": {
+ "lat": 46.6541884,
+ "lng": 16.0754781
+ },
+ "Dol pri Ljubljani": {
+ "lat": 46.0884386,
+ "lng": 14.6424792
+ },
+ "Dolenjske Toplice": {
+ "lat": 45.7558584,
+ "lng": 15.0592333
+ },
+ "Brda": {
+ "lat": 45.9975652,
+ "lng": 13.5270474
+ },
+ "Dobrovnik": {
+ "lat": 46.6516758,
+ "lng": 16.3429113
+ },
+ "Dobrna": {
+ "lat": 46.3356141,
+ "lng": 15.2259732
+ },
+ "Dobje": {
+ "lat": 46.1370037,
+ "lng": 15.394129
+ },
+ "Destrnik": {
+ "lat": 46.4922368,
+ "lng": 15.8777956
+ },
+ "Obcina Crna na Koroskem": {
+ "lat": 46.4704529,
+ "lng": 14.8499998
+ },
+ "Obcina Crensovci": {
+ "lat": 46.5688644,
+ "lng": 16.2941962
+ },
+ "Cerkvenjak": {
+ "lat": 46.5670711,
+ "lng": 15.9429753
+ },
+ "Cerklje na Gorenjskem": {
+ "lat": 46.2531292,
+ "lng": 14.4868829
+ },
+ "Cankova": {
+ "lat": 46.71823699999999,
+ "lng": 16.0197222
+ },
+ "Borovnica": {
+ "lat": 45.9193034,
+ "lng": 14.3640682
+ },
+ "Bohinj": {
+ "lat": 46.2715959,
+ "lng": 13.9563992
+ },
+ "Obcina Bled": {
+ "lat": 46.3511373,
+ "lng": 14.0859017
+ },
+ "Bistrica ob Sotli": {
+ "lat": 46.0565579,
+ "lng": 15.6625947
+ },
+ "Benedikt": {
+ "lat": 46.6075732,
+ "lng": 15.8896942
+ },
+ "Beltinci": {
+ "lat": 46.60791529999999,
+ "lng": 16.2365127
+ },
+ "Obcina Apace": {
+ "lat": 46.6974679,
+ "lng": 15.9102534
+ },
+ "Obcina Razkrizje": {
+ "lat": 46.5226339,
+ "lng": 16.2668638
+ },
+ "Cerkno": {
+ "lat": 46.128954,
+ "lng": 13.9891931
+ },
+ "Dobrepolje": {
+ "lat": 45.8524951,
+ "lng": 14.7083109
+ },
+ "Ankaran": {
+ "lat": 45.57914359999999,
+ "lng": 13.7362855
+ },
+ "Selnica ob Dravi": {
+ "lat": 46.5513918,
+ "lng": 15.492941
+ },
+ "Cirkulane": {
+ "lat": 46.3443632,
+ "lng": 15.9951112
+ },
+ "Komenda": {
+ "lat": 46.2052815,
+ "lng": 14.5391653
+ },
+ "Kostel": {
+ "lat": 45.5083472,
+ "lng": 14.9087946
+ },
+ "Obcina Skocjan": {
+ "lat": 45.9072257,
+ "lng": 15.292258
+ },
+ "": {
+ "lat": 46.151241,
+ "lng": 14.995463
+ }
+ },
+ "BB": {
+ "Christ Church": {
+ "lat": 13.0728209,
+ "lng": -59.5261392
+ },
+ "Saint Andrew": {
+ "lat": 13.2462565,
+ "lng": -59.5651284
+ },
+ "Saint James": {
+ "lat": 13.1842181,
+ "lng": -59.6304714
+ },
+ "Saint John": {
+ "lat": 13.1824334,
+ "lng": -59.5033496
+ },
+ "Saint Philip": {
+ "lat": 13.1256336,
+ "lng": -59.456055
+ },
+ "Saint Peter": {
+ "lat": 13.2600459,
+ "lng": -59.621862
+ },
+ "Saint Michael": {
+ "lat": 13.1132219,
+ "lng": -59.59880889999999
+ },
+ "Saint George": {
+ "lat": 13.1401378,
+ "lng": -59.54611249999999
+ },
+ "Saint Thomas": {
+ "lat": 13.1740547,
+ "lng": -59.5827556
+ },
+ "Saint Joseph": {
+ "lat": 13.2005084,
+ "lng": -59.53932349999999
+ },
+ "": {
+ "lat": 13.193887,
+ "lng": -59.543198
+ }
+ },
+ "CV": {
+ "Brava": {
+ "lat": 14.8444438,
+ "lng": -24.7024179
+ },
+ "Maio": {
+ "lat": 15.2003098,
+ "lng": -23.1679793
+ },
+ "Ribeira Brava": {
+ "lat": 16.616129,
+ "lng": -24.2969999
+ },
+ "Tarrafal": {
+ "lat": 15.2760578,
+ "lng": -23.7484077
+ },
+ "Ribeira Grande": {
+ "lat": 17.1820718,
+ "lng": -25.0658748
+ },
+ "Tarrafal de São Nicolau": {
+ "lat": 16.5636498,
+ "lng": -24.354942
+ },
+ "São Filipe": {
+ "lat": 14.8951679,
+ "lng": -24.4945636
+ },
+ "São Domingos": {
+ "lat": 15.0286165,
+ "lng": -23.563922
+ },
+ "Sal": {
+ "lat": 16.7266152,
+ "lng": -22.9297109
+ },
+ "Santa Cruz": {
+ "lat": 15.1027292,
+ "lng": -23.5609459
+ },
+ "Boa Vista": {
+ "lat": 16.0950108,
+ "lng": -22.8078335
+ },
+ "Praia": {
+ "lat": 14.93305,
+ "lng": -23.5133267
+ },
+ "Porto Novo": {
+ "lat": 17.0215176,
+ "lng": -25.0673575
+ },
+ "Mosteiros": {
+ "lat": 15.0311753,
+ "lng": -24.3252291
+ },
+ "Paul": {
+ "lat": 17.1005359,
+ "lng": -24.9872821
+ },
+ "São Salvador do Mundo": {
+ "lat": 15.088764,
+ "lng": -23.6383187
+ },
+ "São Lourenço dos Órgãos": {
+ "lat": 15.0537841,
+ "lng": -23.6085612
+ },
+ "São Vicente": {
+ "lat": 16.8341271,
+ "lng": -24.9279547
+ },
+ "São Miguel": {
+ "lat": 15.1919609,
+ "lng": -23.6442701
+ },
+ "Santa Catarina do Fogo": {
+ "lat": 14.9309104,
+ "lng": -24.3222577
+ },
+ "Ribeira Grande de Santiago": {
+ "lat": 14.9830298,
+ "lng": -23.6561725
+ },
+ "Santa Catarina": {
+ "lat": 15.0992532,
+ "lng": -23.6918783
+ },
+ "": {
+ "lat": 16.002082,
+ "lng": -24.013197
+ }
+ },
+ "GY": {
+ "Essequibo Islands-West Demerara Region": {
+ "lat": 6.5720132,
+ "lng": -58.4629997
+ },
+ "East Berbice-Corentyne Region": {
+ "lat": 2.7477922,
+ "lng": -57.4627259
+ },
+ "Mahaica-Berbice Region": {
+ "lat": 6.238496,
+ "lng": -57.9162555
+ },
+ "Pomeroon-Supenaam Region": {
+ "lat": 7.1294166,
+ "lng": -58.9206295
+ },
+ "Potaro-Siparuni Region": {
+ "lat": 4.7855853,
+ "lng": -59.28799770000001
+ },
+ "Demerara-Mahaica Region": {
+ "lat": 6.546425999999999,
+ "lng": -58.0982046
+ },
+ "Barima-Waini Region": {
+ "lat": 7.488241899999999,
+ "lng": -59.6564494
+ },
+ "Upper Demerara-Berbice Region": {
+ "lat": 5.3064879,
+ "lng": -58.18929209999999
+ },
+ "Upper Takutu-Upper Essequibo Region": {
+ "lat": 2.9239595,
+ "lng": -58.73736339999999
+ },
+ "Cuyuni-Mazaruni Region": {
+ "lat": 6.4642141,
+ "lng": -60.21107519999999
+ },
+ "": {
+ "lat": 4.860416,
+ "lng": -58.93018
+ }
+ },
+ "SR": {
+ "Distrikt Wanica": {
+ "lat": 5.7323762,
+ "lng": -55.2701235
+ },
+ "Distrikt Nickerie": {
+ "lat": 5.5855469,
+ "lng": -56.83111169999999
+ },
+ "Distrikt Coronie": {
+ "lat": 5.6943271,
+ "lng": -56.2929381
+ },
+ "Distrikt Para": {
+ "lat": 5.481731799999999,
+ "lng": -55.2259207
+ },
+ "Distrikt Paramaribo": {
+ "lat": 5.848920499999999,
+ "lng": -55.1596592
+ },
+ "Distrikt Commewijne": {
+ "lat": 5.740211,
+ "lng": -54.8731219
+ },
+ "Distrikt Marowijne": {
+ "lat": 5.6268128,
+ "lng": -54.25931180000001
+ },
+ "Distrikt Saramacca": {
+ "lat": 5.7240813,
+ "lng": -55.6689636
+ },
+ "Distrikt Brokopondo": {
+ "lat": 4.7710247,
+ "lng": -55.0493375
+ },
+ "Distrikt Sipaliwini": {
+ "lat": 3.6567382,
+ "lng": -56.2035387
+ },
+ "": {
+ "lat": 3.919305,
+ "lng": -56.027783
+ }
+ },
+ "BR": {
+ "Paraíba": {
+ "lat": -7.239960900000001,
+ "lng": -36.7819505
+ },
+ "Piaui": {
+ "lat": -8.322948799999999,
+ "lng": -43.1747162
+ },
+ "Pernambuco": {
+ "lat": -8.8137173,
+ "lng": -36.954107
+ },
+ "Tocantins": {
+ "lat": -11.4098737,
+ "lng": -48.71914229999999
+ },
+ "Maranhao": {
+ "lat": -5.080419,
+ "lng": -45.6007108
+ },
+ "Para": {
+ "lat": -6.207102,
+ "lng": -52.70279559999999
+ },
+ "Amapa": {
+ "lat": 1.4441146,
+ "lng": -52.0215415
+ },
+ "Ceara": {
+ "lat": -5.4983977,
+ "lng": -39.3206241
+ },
+ "Alagoas": {
+ "lat": -9.5713058,
+ "lng": -36.7819505
+ },
+ "Amazonas": {
+ "lat": -3.4168427,
+ "lng": -65.8560646
+ },
+ "Rio Grande do Norte": {
+ "lat": -5.402580299999999,
+ "lng": -36.954107
+ },
+ "Bahia": {
+ "lat": -11.4098737,
+ "lng": -41.2808577
+ },
+ "Sergipe": {
+ "lat": -10.6738878,
+ "lng": -37.4681396
+ },
+ "Roraima": {
+ "lat": 1.5957682,
+ "lng": -60.58206759999999
+ },
+ "Santa Catarina": {
+ "lat": -26.928572,
+ "lng": -49.36531489999999
+ },
+ "Sao Paulo": {
+ "lat": -23.5557714,
+ "lng": -46.6395571
+ },
+ "Parana": {
+ "lat": -25.2520888,
+ "lng": -52.0215415
+ },
+ "Rio de Janeiro": {
+ "lat": -22.9068467,
+ "lng": -43.1728965
+ },
+ "Minas Gerais": {
+ "lat": -17.930178,
+ "lng": -43.7908453
+ },
+ "Espirito Santo": {
+ "lat": -19.1834229,
+ "lng": -40.3088626
+ },
+ "Rio Grande do Sul": {
+ "lat": -29.3646459,
+ "lng": -51.6657692
+ },
+ "Goias": {
+ "lat": -15.7050424,
+ "lng": -49.36531489999999
+ },
+ "Mato Grosso": {
+ "lat": -12.6818712,
+ "lng": -56.921099
+ },
+ "Mato Grosso do Sul": {
+ "lat": -20.7722295,
+ "lng": -54.7851531
+ },
+ "Federal District": {
+ "lat": -15.826691,
+ "lng": -47.92182039999999
+ },
+ "Acre": {
+ "lat": -9.0237964,
+ "lng": -70.81199529999999
+ },
+ "Rondonia": {
+ "lat": -11.5057341,
+ "lng": -63.580611
+ },
+ "": {
+ "lat": -14.235004,
+ "lng": -51.92528
+ }
+ },
+ "GL": {
+ "Avannaata": {
+ "lat": 75.9645208,
+ "lng": -53.3651186
+ },
+ "Qeqqata": {
+ "lat": 66.1763879,
+ "lng": -48.9906533
+ },
+ "Qeqertalik": {
+ "lat": 68.2055932,
+ "lng": -46.4348782
+ },
+ "Kujalleq": {
+ "lat": 61.3850674,
+ "lng": -44.4319034
+ },
+ "Sermersooq": {
+ "lat": 68.09179809999999,
+ "lng": -39.2845123
+ },
+ "": {
+ "lat": 71.706936,
+ "lng": -42.604303
+ }
+ },
+ "PM": {
+ "Commune de Saint-Pierre": {
+ "lat": 46.7809057,
+ "lng": -56.1720053
+ },
+ "Miquelon-Langlade": {
+ "lat": 46.8454114,
+ "lng": -56.30752810000001
+ },
+ "": {
+ "lat": 46.941936,
+ "lng": -56.27111
+ }
+ },
+ "AR": {
+ "Buenos Aires": {
+ "lat": -34.6036844,
+ "lng": -58.3815591
+ },
+ "Misiones": {
+ "lat": -26.9377146,
+ "lng": -54.4342138
+ },
+ "Buenos Aires F.D.": {
+ "lat": -34.6036844,
+ "lng": -58.3815591
+ },
+ "Entre Rios": {
+ "lat": -32.5175643,
+ "lng": -59.1041758
+ },
+ "Santa Fe": {
+ "lat": -31.6106578,
+ "lng": -60.697294
+ },
+ "Formosa": {
+ "lat": -26.1857768,
+ "lng": -58.1755669
+ },
+ "Corrientes": {
+ "lat": -27.4692131,
+ "lng": -58.8306349
+ },
+ "Chaco": {
+ "lat": -26.5857656,
+ "lng": -60.9540073
+ },
+ "Neuquen": {
+ "lat": -38.9516784,
+ "lng": -68.0591888
+ },
+ "Jujuy": {
+ "lat": -24.1857864,
+ "lng": -65.2994767
+ },
+ "Tucuman": {
+ "lat": -26.8082848,
+ "lng": -65.2175903
+ },
+ "La Pampa": {
+ "lat": -37.8956594,
+ "lng": -65.0957792
+ },
+ "Cordoba": {
+ "lat": -31.42008329999999,
+ "lng": -64.1887761
+ },
+ "Mendoza": {
+ "lat": -32.8894587,
+ "lng": -68.8458386
+ },
+ "La Rioja": {
+ "lat": -29.4134538,
+ "lng": -66.8564579
+ },
+ "Santiago del Estero": {
+ "lat": -27.7833574,
+ "lng": -64.264167
+ },
+ "Rio Negro": {
+ "lat": -40.00988417743898,
+ "lng": -65.39272061835581
+ },
+ "San Juan": {
+ "lat": -31.5351074,
+ "lng": -68.5385941
+ },
+ "Santa Cruz": {
+ "lat": -48.7736825,
+ "lng": -69.1917167
+ },
+ "Salta": {
+ "lat": -24.7821269,
+ "lng": -65.4231976
+ },
+ "Tierra del Fuego": {
+ "lat": -54.3083548,
+ "lng": -67.7451565
+ },
+ "Chubut": {
+ "lat": -43.6846192,
+ "lng": -69.2745537
+ },
+ "Catamarca": {
+ "lat": -28.469581,
+ "lng": -65.7795441
+ },
+ "San Luis": {
+ "lat": -33.3017267,
+ "lng": -66.3377522
+ },
+ "": {
+ "lat": -38.416097,
+ "lng": -63.616672
+ }
+ },
+ "PY": {
+ "Departamento de Caazapa": {
+ "lat": -26.2260696,
+ "lng": -56.0249982
+ },
+ "Departamento Central": {
+ "lat": -25.5628115,
+ "lng": -57.5079912
+ },
+ "Departamento de Canindeyu": {
+ "lat": -24.1378735,
+ "lng": -55.6689636
+ },
+ "Departamento del Guaira": {
+ "lat": -25.8810932,
+ "lng": -56.2929381
+ },
+ "Departamento de Neembucu": {
+ "lat": -27.0299114,
+ "lng": -57.825395
+ },
+ "Departamento de Presidente Hayes": {
+ "lat": -23.3512605,
+ "lng": -58.73736339999999
+ },
+ "Departamento del Amambay": {
+ "lat": -22.5590272,
+ "lng": -56.0249982
+ },
+ "Departamento de Misiones": {
+ "lat": -26.8433512,
+ "lng": -57.10131879999999
+ },
+ "Departamento del Alto Parana": {
+ "lat": -25.6075546,
+ "lng": -54.9611836
+ },
+ "Departamento de San Pedro": {
+ "lat": -24.1948668,
+ "lng": -56.56164700000001
+ },
+ "Departamento de la Cordillera": {
+ "lat": -25.2289491,
+ "lng": -57.0111681
+ },
+ "Departamento de Paraguari": {
+ "lat": -26.174027,
+ "lng": -57.10131879999999
+ },
+ "Departamento de Itapua": {
+ "lat": -26.7923623,
+ "lng": -55.6689636
+ },
+ "Departamento de Boqueron": {
+ "lat": -21.7449254,
+ "lng": -60.9540073
+ },
+ "Departamento de Caaguazu": {
+ "lat": -25.2281911,
+ "lng": -56.0249982
+ },
+ "Departamento de Alto Paraguay": {
+ "lat": -20.0852508,
+ "lng": -59.4720904
+ },
+ "Departamento de Concepcion": {
+ "lat": -22.811926,
+ "lng": -57.10131879999999
+ },
+ "Asuncion": {
+ "lat": -25.2637399,
+ "lng": -57.57592599999999
+ },
+ "": {
+ "lat": -23.442503,
+ "lng": -58.443832
+ }
+ },
+ "UY": {
+ "Durazno Department": {
+ "lat": -33.0232454,
+ "lng": -56.0284644
+ },
+ "Rivera Department": {
+ "lat": -31.4817421,
+ "lng": -55.2435759
+ },
+ "Treinta y Tres Department": {
+ "lat": -33.0685086,
+ "lng": -54.2858627
+ },
+ "Florida": {
+ "lat": -34.0944617,
+ "lng": -56.2185912
+ },
+ "Cerro Largo": {
+ "lat": -32.4411032,
+ "lng": -54.35217530000001
+ },
+ "Montevideo Department": {
+ "lat": -34.8181587,
+ "lng": -56.2138256
+ },
+ "Flores Department": {
+ "lat": -33.5733753,
+ "lng": -56.8945028
+ },
+ "Canelones": {
+ "lat": -34.5246844,
+ "lng": -56.28121040000001
+ },
+ "Colonia": {
+ "lat": -34.1294678,
+ "lng": -57.66051840000001
+ },
+ "Tacuarembó Department": {
+ "lat": -32.1082207,
+ "lng": -55.77085779999999
+ },
+ "Soriano": {
+ "lat": -33.5102792,
+ "lng": -57.7498103
+ },
+ "Lavalleja": {
+ "lat": -33.9226175,
+ "lng": -54.9765794
+ },
+ "San José Department": {
+ "lat": -34.3086518,
+ "lng": -56.72563659999999
+ },
+ "Maldonado": {
+ "lat": -34.9027462,
+ "lng": -54.9491154
+ },
+ "Salto Department": {
+ "lat": -31.3275473,
+ "lng": -57.0174129
+ },
+ "Rocha Department": {
+ "lat": -33.9690081,
+ "lng": -54.021485
+ },
+ "Paysandú Department": {
+ "lat": -32.0667366,
+ "lng": -57.3364789
+ },
+ "Río Negro Department": {
+ "lat": -32.7676356,
+ "lng": -57.4295207
+ },
+ "Artigas": {
+ "lat": -30.4043287,
+ "lng": -56.4692371
+ },
+ "": {
+ "lat": -32.522779,
+ "lng": -55.765835
+ }
+ },
+ "VE": {
+ "Nueva Esparta": {
+ "lat": 10.9970723,
+ "lng": -63.91132959999999
+ },
+ "Distrito Federal": {
+ "lat": 10.5004352,
+ "lng": -66.9511459
+ },
+ "Carabobo": {
+ "lat": 10.1176433,
+ "lng": -68.0477509
+ },
+ "Anzoátegui": {
+ "lat": 8.5913073,
+ "lng": -63.95861110000001
+ },
+ "Barinas": {
+ "lat": 8.6231498,
+ "lng": -70.2371045
+ },
+ "Zulia": {
+ "lat": 10.2910237,
+ "lng": -72.1416132
+ },
+ "Yaracuy": {
+ "lat": 10.339389,
+ "lng": -68.81088489999999
+ },
+ "Aragua": {
+ "lat": 10.0635758,
+ "lng": -67.2847875
+ },
+ "Portuguesa": {
+ "lat": 9.094399899999999,
+ "lng": -69.097023
+ },
+ "Guárico": {
+ "lat": 8.7489309,
+ "lng": -66.2367172
+ },
+ "Bolívar": {
+ "lat": 6.3585216,
+ "lng": -63.580611
+ },
+ "Estado Trujillo": {
+ "lat": 9.4302528,
+ "lng": -70.52649339999999
+ },
+ "Delta Amacuro": {
+ "lat": 8.8499307,
+ "lng": -61.14031960000001
+ },
+ "Falcón": {
+ "lat": 11.1810674,
+ "lng": -69.8597406
+ },
+ "Cojedes": {
+ "lat": 9.3816682,
+ "lng": -68.3339275
+ },
+ "Táchira": {
+ "lat": 7.9137001,
+ "lng": -72.1416132
+ },
+ "Lara": {
+ "lat": 10.1537842,
+ "lng": -69.8597406
+ },
+ "Sucre": {
+ "lat": 10.4055886,
+ "lng": -63.297505
+ },
+ "Miranda": {
+ "lat": 10.2509303,
+ "lng": -66.4271499
+ },
+ "Amazonas": {
+ "lat": 2.8101413,
+ "lng": -65.0957792
+ },
+ "Monagas": {
+ "lat": 9.3241652,
+ "lng": -63.0147578
+ },
+ "Mérida": {
+ "lat": 8.5698244,
+ "lng": -71.1804988
+ },
+ "Vargas": {
+ "lat": 10.5890015,
+ "lng": -66.7367345
+ },
+ "Dependencias Federales": {
+ "lat": 10.9377053,
+ "lng": -65.35695729999999
+ },
+ "Apure": {
+ "lat": 6.926948299999999,
+ "lng": -68.52471489999999
+ },
+ "": {
+ "lat": 6.42375,
+ "lng": -66.58973
+ }
+ },
+ "MX": {
+ "Tamaulipas": {
+ "lat": 24.26694,
+ "lng": -98.8362755
+ },
+ "San Luis Potosí": {
+ "lat": 22.1564699,
+ "lng": -100.9855409
+ },
+ "México": {
+ "lat": 23.634501,
+ "lng": -102.552784
+ },
+ "Guerrero": {
+ "lat": 17.4391926,
+ "lng": -99.54509739999999
+ },
+ "Veracruz": {
+ "lat": 19.173773,
+ "lng": -96.1342241
+ },
+ "Hidalgo": {
+ "lat": 20.0910963,
+ "lng": -98.7623874
+ },
+ "Puebla": {
+ "lat": 19.0414398,
+ "lng": -98.2062727
+ },
+ "Morelos": {
+ "lat": 18.6813049,
+ "lng": -99.1013498
+ },
+ "Oaxaca": {
+ "lat": 17.0731842,
+ "lng": -96.7265889
+ },
+ "Yucatán": {
+ "lat": 20.7098786,
+ "lng": -89.0943377
+ },
+ "Chiapas": {
+ "lat": 16.7569318,
+ "lng": -93.12923529999999
+ },
+ "Mexico City": {
+ "lat": 19.4326077,
+ "lng": -99.133208
+ },
+ "Tlaxcala": {
+ "lat": 19.318154,
+ "lng": -98.2374954
+ },
+ "Tabasco": {
+ "lat": 17.8409173,
+ "lng": -92.6189273
+ },
+ "Quintana Roo": {
+ "lat": 19.1817393,
+ "lng": -88.4791376
+ },
+ "Querétaro": {
+ "lat": 20.5887932,
+ "lng": -100.3898881
+ },
+ "Campeche": {
+ "lat": 19.8301251,
+ "lng": -90.5349087
+ },
+ "Nuevo León": {
+ "lat": 25.592172,
+ "lng": -99.99619469999999
+ },
+ "Sonora": {
+ "lat": 29.2972247,
+ "lng": -110.3308814
+ },
+ "Zacatecas": {
+ "lat": 22.7708555,
+ "lng": -102.5832426
+ },
+ "Chihuahua": {
+ "lat": 28.6333754,
+ "lng": -106.0697945
+ },
+ "Sinaloa": {
+ "lat": 25.8226854,
+ "lng": -108.2216704
+ },
+ "Jalisco": {
+ "lat": 20.6595382,
+ "lng": -103.3494376
+ },
+ "Durango": {
+ "lat": 24.0277202,
+ "lng": -104.6531759
+ },
+ "Baja California": {
+ "lat": 30.8406338,
+ "lng": -115.2837585
+ },
+ "Michoacán": {
+ "lat": 19.5665192,
+ "lng": -101.7068294
+ },
+ "Nayarit": {
+ "lat": 21.7513844,
+ "lng": -104.8454619
+ },
+ "Guanajuato": {
+ "lat": 20.9170187,
+ "lng": -101.1617356
+ },
+ "Coahuila": {
+ "lat": 27.058676,
+ "lng": -101.7068294
+ },
+ "Colima": {
+ "lat": 19.2452342,
+ "lng": -103.7240868
+ },
+ "Baja California Sur": {
+ "lat": 26.0444446,
+ "lng": -111.6660725
+ },
+ "Aguascalientes": {
+ "lat": 21.8852562,
+ "lng": -102.2915677
+ },
+ "": {
+ "lat": 23.634501,
+ "lng": -102.552784
+ }
+ },
+ "JM": {
+ "Saint Thomas": {
+ "lat": 17.9700261,
+ "lng": -76.4331698
+ },
+ "Trelawny": {
+ "lat": 18.3526143,
+ "lng": -77.6077865
+ },
+ "Saint Andrew": {
+ "lat": 18.0391345,
+ "lng": -76.7567368
+ },
+ "Saint Elizabeth": {
+ "lat": 18.0788461,
+ "lng": -77.69941969999999
+ },
+ "Parish of Saint Ann": {
+ "lat": 18.3281428,
+ "lng": -77.2405153
+ },
+ "Saint Catherine": {
+ "lat": 18.0364134,
+ "lng": -77.0564464
+ },
+ "Clarendon": {
+ "lat": 17.9557183,
+ "lng": -77.2405153
+ },
+ "Saint Mary": {
+ "lat": 18.3092711,
+ "lng": -76.964306
+ },
+ "Portland": {
+ "lat": 18.0844274,
+ "lng": -76.4100267
+ },
+ "Westmoreland": {
+ "lat": 18.2944378,
+ "lng": -78.1564432
+ },
+ "Saint James": {
+ "lat": 18.3923184,
+ "lng": -77.85959629999999
+ },
+ "Manchester": {
+ "lat": 18.0669654,
+ "lng": -77.5160788
+ },
+ "Hanover": {
+ "lat": 18.4097707,
+ "lng": -78.13363799999999
+ },
+ "Kingston": {
+ "lat": 18.0178743,
+ "lng": -76.8099041
+ },
+ "": {
+ "lat": 18.109581,
+ "lng": -77.297508
+ }
+ },
+ "DO": {
+ "Provincia de Monte Plata": {
+ "lat": 18.8080878,
+ "lng": -69.7869146
+ },
+ "Provincia de Hermanas Mirabal": {
+ "lat": 19.3747559,
+ "lng": -70.35132349999999
+ },
+ "Provincia de Baoruco": {
+ "lat": 18.4879898,
+ "lng": -71.4182249
+ },
+ "Provincia de Santiago": {
+ "lat": 19.45409,
+ "lng": -70.6922025
+ },
+ "Provincia de San Cristobal": {
+ "lat": 18.4615358,
+ "lng": -70.1217652
+ },
+ "Provincia Espaillat": {
+ "lat": 19.6277658,
+ "lng": -70.2786775
+ },
+ "Provincia de La Altagracia": {
+ "lat": 18.5850236,
+ "lng": -68.62010719999999
+ },
+ "Provincia de Dajabon": {
+ "lat": 19.4196997,
+ "lng": -71.6144554
+ },
+ "Puerto Plata": {
+ "lat": 19.7807686,
+ "lng": -70.6871091
+ },
+ "Nacional": {
+ "lat": 18.735693,
+ "lng": -70.162651
+ },
+ "Provincia de El Seibo": {
+ "lat": 18.7658496,
+ "lng": -69.040668
+ },
+ "Provincia de Barahona": {
+ "lat": 18.2139066,
+ "lng": -71.1043759
+ },
+ "Samaná": {
+ "lat": 19.2030757,
+ "lng": -69.3387664
+ },
+ "Provincia de San Pedro de Macoris": {
+ "lat": 18.4685065,
+ "lng": -69.2973372
+ },
+ "Provincia de San Juan": {
+ "lat": 18.8073919,
+ "lng": -71.2293466
+ },
+ "Provincia de San Jose de Ocoa": {
+ "lat": 18.5465792,
+ "lng": -70.50774899999999
+ },
+ "Provincia de Santo Domingo": {
+ "lat": 18.5104253,
+ "lng": -69.8404054
+ },
+ "Provincia Duarte": {
+ "lat": 19.2090823,
+ "lng": -70.02700039999999
+ },
+ "Provincia de Monte Cristi": {
+ "lat": 19.7396899,
+ "lng": -71.44339839999999
+ },
+ "Provincia de Santiago Rodriguez": {
+ "lat": 19.4786174,
+ "lng": -71.3398271
+ },
+ "Provincia Maria Trinidad Sanchez": {
+ "lat": 19.3734597,
+ "lng": -69.85144389999999
+ },
+ "Provincia de Elias Pina": {
+ "lat": 19.0524685,
+ "lng": -71.6198686
+ },
+ "Provincia de Pedernales": {
+ "lat": 17.8537626,
+ "lng": -71.3303209
+ },
+ "Provincia de Azua": {
+ "lat": 18.4552709,
+ "lng": -70.7380928
+ },
+ "Provincia de Valverde": {
+ "lat": 19.5881221,
+ "lng": -70.98033099999999
+ },
+ "Provincia de Monsenor Nouel": {
+ "lat": 18.9215234,
+ "lng": -70.3836815
+ },
+ "Provincia de Peravia": {
+ "lat": 18.2786594,
+ "lng": -70.33358869999999
+ },
+ "Provincia de La Vega": {
+ "lat": 19.2211554,
+ "lng": -70.5288753
+ },
+ "Provincia de Independencia": {
+ "lat": 18.3785651,
+ "lng": -71.5232874
+ },
+ "Provincia de La Romana": {
+ "lat": 18.4310271,
+ "lng": -68.98373730000002
+ },
+ "Provincia Sanchez Ramirez": {
+ "lat": 19.052706,
+ "lng": -70.1492264
+ },
+ "Provincia de Hato Mayor": {
+ "lat": 18.7635799,
+ "lng": -69.2557637
+ },
+ "": {
+ "lat": 18.735693,
+ "lng": -70.162651
+ }
+ },
+ "BQ": {
+ "Saba": {
+ "lat": 17.6354642,
+ "lng": -63.2326763
+ },
+ "Sint Eustatius": {
+ "lat": 17.4890306,
+ "lng": -62.973555
+ },
+ "Bonaire": {
+ "lat": 12.2018902,
+ "lng": -68.26238219999999
+ },
+ "": {
+ "lat": 14.8583535,
+ "lng": -67.152135
+ }
+ },
+ "CU": {
+ "Matanzas Province": {
+ "lat": 22.5767123,
+ "lng": -81.3399414
+ },
+ "Artemisa": {
+ "lat": 22.8159074,
+ "lng": -82.7589859
+ },
+ "Provincia de Santiago de Cuba": {
+ "lat": 20.2246196,
+ "lng": -75.8069082
+ },
+ "Havana": {
+ "lat": 23.1135925,
+ "lng": -82.3665956
+ },
+ "Las Tunas": {
+ "lat": 20.957938,
+ "lng": -76.9527836
+ },
+ "Provincia de Pinar del Rio": {
+ "lat": 22.4076256,
+ "lng": -83.8473015
+ },
+ "Holguín Province": {
+ "lat": 20.7992817,
+ "lng": -75.9927652
+ },
+ "Guantánamo Province": {
+ "lat": 20.1455917,
+ "lng": -74.8741045
+ },
+ "Cienfuegos Province": {
+ "lat": 22.2587327,
+ "lng": -80.7214417
+ },
+ "Provincia de Camagueey": {
+ "lat": 21.2167247,
+ "lng": -77.7452081
+ },
+ "Granma Province": {
+ "lat": 20.3844902,
+ "lng": -76.64127119999999
+ },
+ "Ciego de Ávila Province": {
+ "lat": 21.9329515,
+ "lng": -78.5660852
+ },
+ "": {
+ "lat": 21.521757,
+ "lng": -77.781167
+ }
+ },
+ "BS": {
+ "West Grand Bahama District": {
+ "lat": 26.659447,
+ "lng": -78.52065
+ },
+ "North Eleuthera": {
+ "lat": 25.4647517,
+ "lng": -76.675922
+ },
+ "South Eleuthera": {
+ "lat": 24.7708562,
+ "lng": -76.2131474
+ },
+ "East Grand Bahama District": {
+ "lat": 26.6582823,
+ "lng": -78.2248291
+ },
+ "Spanish Wells District": {
+ "lat": 25.5461164,
+ "lng": -76.7711626
+ },
+ "New Providence District": {
+ "lat": 25.0443256,
+ "lng": -77.3503559
+ },
+ "Inagua": {
+ "lat": 21.0656066,
+ "lng": -73.323708
+ },
+ "Central Abaco District": {
+ "lat": 26.3555029,
+ "lng": -77.1485163
+ },
+ "City of Freeport District": {
+ "lat": 26.5333159,
+ "lng": -78.6429019
+ },
+ "Harbour Island": {
+ "lat": 25.50011,
+ "lng": -76.6340511
+ },
+ "North Abaco District": {
+ "lat": 26.7871697,
+ "lng": -77.4357739
+ },
+ "San Salvador District": {
+ "lat": 24.0775546,
+ "lng": -74.4760088
+ },
+ "Long Island": {
+ "lat": 23.1764239,
+ "lng": -75.0961549
+ },
+ "Berry Islands District": {
+ "lat": 25.7236234,
+ "lng": -77.8310104
+ },
+ "Central Andros District": {
+ "lat": 24.4688482,
+ "lng": -77.973865
+ },
+ "Black Point District": {
+ "lat": 24.0861912,
+ "lng": -76.3955603
+ },
+ "North Andros District": {
+ "lat": 24.7063805,
+ "lng": -78.0195387
+ },
+ "Grand Cay District": {
+ "lat": 27.2162615,
+ "lng": -78.3230559
+ },
+ "": {
+ "lat": 25.03428,
+ "lng": -77.39628
+ }
+ },
+ "BM": {
+ "Hamilton": {
+ "lat": 32.2950716,
+ "lng": -64.7842417
+ },
+ "Southampton Parish": {
+ "lat": 32.2540095,
+ "lng": -64.8259058
+ },
+ "Sandys Parish": {
+ "lat": 32.2999528,
+ "lng": -64.8674103
+ },
+ "Saint George": {
+ "lat": 32.3811588,
+ "lng": -64.6821494
+ },
+ "Pembroke Parish": {
+ "lat": 32.3007672,
+ "lng": -64.796263
+ },
+ "Paget": {
+ "lat": 32.281074,
+ "lng": -64.7784787
+ },
+ "Saint George's Parish": {
+ "lat": 32.3668615,
+ "lng": -64.68364609999999
+ },
+ "Hamilton city": {
+ "lat": 32.2950716,
+ "lng": -64.7842417
+ },
+ "Devonshire Parish": {
+ "lat": 32.3038062,
+ "lng": -64.7606954
+ },
+ "Smith's Parish": {
+ "lat": 32.3133966,
+ "lng": -64.7310588
+ },
+ "": {
+ "lat": 32.321384,
+ "lng": -64.75737
+ }
+ },
+ "TT": {
+ "Port of Spain": {
+ "lat": 10.6603196,
+ "lng": -61.5085625
+ },
+ "Princes Town": {
+ "lat": 10.2659505,
+ "lng": -61.37643809999999
+ },
+ "Tunapuna/Piarco": {
+ "lat": 10.6859096,
+ "lng": -61.30352480000001
+ },
+ "Sangre Grande": {
+ "lat": 10.5852939,
+ "lng": -61.1315813
+ },
+ "Tobago": {
+ "lat": 11.2336911,
+ "lng": -60.69889089999999
+ },
+ "Siparia": {
+ "lat": 10.0910494,
+ "lng": -61.5252827
+ },
+ "San Juan/Laventille": {
+ "lat": 10.6908578,
+ "lng": -61.4552213
+ },
+ "San Fernando": {
+ "lat": 10.2819954,
+ "lng": -61.4668769
+ },
+ "Mayaro": {
+ "lat": 10.2803437,
+ "lng": -61.0296696
+ },
+ "Couva-Tabaquite-Talparo": {
+ "lat": 10.4297145,
+ "lng": -61.373521
+ },
+ "Point Fortin": {
+ "lat": 10.1702737,
+ "lng": -61.67133860000001
+ },
+ "Diego Martin": {
+ "lat": 10.7362286,
+ "lng": -61.5544836
+ },
+ "Penal/Debe": {
+ "lat": 10.1337402,
+ "lng": -61.44354740000001
+ },
+ "Chaguanas": {
+ "lat": 10.5168387,
+ "lng": -61.4114482
+ },
+ "Borough of Arima": {
+ "lat": 10.6273078,
+ "lng": -61.2743688
+ },
+ "": {
+ "lat": 10.691803,
+ "lng": -61.222503
+ }
+ },
+ "KN": {
+ "Trinity Palmetto Point": {
+ "lat": 17.3063519,
+ "lng": -62.7617837
+ },
+ "Saint Anne Sandy Point": {
+ "lat": 17.3725333,
+ "lng": -62.8441133
+ },
+ "Middle Island": {
+ "lat": 17.3348813,
+ "lng": -62.80882510000001
+ },
+ "Christ Church Nichola Town": {
+ "lat": 17.3604812,
+ "lng": -62.7617837
+ },
+ "Saint Paul Capesterre": {
+ "lat": 17.4016683,
+ "lng": -62.82573319999999
+ },
+ "Saint James Windward": {
+ "lat": 17.1769633,
+ "lng": -62.57960259999999
+ },
+ "Saint Peter Basseterre": {
+ "lat": 17.3102911,
+ "lng": -62.7147533
+ },
+ "Saint George Gingerland": {
+ "lat": 17.1257759,
+ "lng": -62.5619811
+ },
+ "Saint Thomas Lowland": {
+ "lat": 17.1650513,
+ "lng": -62.6089753
+ },
+ "Saint Paul Charlestown": {
+ "lat": 17.1346297,
+ "lng": -62.6133816
+ },
+ "Saint Mary Cayon": {
+ "lat": 17.3462071,
+ "lng": -62.73826709999999
+ },
+ "Saint George Basseterre": {
+ "lat": 17.2350612,
+ "lng": -62.6442284
+ },
+ "": {
+ "lat": 17.357822,
+ "lng": -62.782998
+ }
+ },
+ "DM": {
+ "Saint Joseph": {
+ "lat": 15.406483,
+ "lng": -61.4216609
+ },
+ "Saint George": {
+ "lat": 15.3087847,
+ "lng": -61.33851909999999
+ },
+ "Saint David": {
+ "lat": 15.4081998,
+ "lng": -61.29186180000001
+ },
+ "Saint John": {
+ "lat": 15.5763757,
+ "lng": -61.4318742
+ },
+ "Saint Luke": {
+ "lat": 15.254587,
+ "lng": -61.36768679999999
+ },
+ "Saint Andrew": {
+ "lat": 15.5542442,
+ "lng": -61.3501856
+ },
+ "Saint Peter": {
+ "lat": 15.5098288,
+ "lng": -61.4493842
+ },
+ "Saint Patrick": {
+ "lat": 15.2811117,
+ "lng": -61.29186180000001
+ },
+ "": {
+ "lat": 15.414999,
+ "lng": -61.370976
+ }
+ },
+ "AG": {
+ "Parish of Saint John": {
+ "lat": 17.1086478,
+ "lng": -61.82922060000001
+ },
+ "Parish of Saint George": {
+ "lat": 17.1078489,
+ "lng": -61.7882743
+ },
+ "Parish of Saint Mary": {
+ "lat": 17.0512064,
+ "lng": -61.87602829999999
+ },
+ "Parish of Saint Paul": {
+ "lat": 17.0371588,
+ "lng": -61.7824256
+ },
+ "Parish of Saint Philip": {
+ "lat": 17.0480636,
+ "lng": -61.71225699999999
+ },
+ "Barbuda": {
+ "lat": 17.6266242,
+ "lng": -61.77130279999999
+ },
+ "Parish of Saint Peter": {
+ "lat": 17.0980665,
+ "lng": -61.75903289999999
+ },
+ "": {
+ "lat": 17.060816,
+ "lng": -61.796428
+ }
+ },
+ "LC": {
+ "Vieux-Fort": {
+ "lat": 13.720608,
+ "lng": -60.94964329999999
+ },
+ "Micoud": {
+ "lat": 13.8211871,
+ "lng": -60.90019339999999
+ },
+ "Castries": {
+ "lat": 14.0110158,
+ "lng": -60.98972389999999
+ },
+ "Laborie": {
+ "lat": 13.7522783,
+ "lng": -60.9932889
+ },
+ "Gros-Islet": {
+ "lat": 14.0843578,
+ "lng": -60.9452794
+ },
+ "Choiseul": {
+ "lat": 13.7750154,
+ "lng": -61.04859099999999
+ },
+ "Canaries": {
+ "lat": 13.9047039,
+ "lng": -61.0667869
+ },
+ "": {
+ "lat": 13.909444,
+ "lng": -60.978893
+ }
+ },
+ "VC": {
+ "Parish of Saint George": {
+ "lat": 13.1698266,
+ "lng": -61.1985879
+ },
+ "Parish of Saint Patrick": {
+ "lat": 13.2379389,
+ "lng": -61.24521799999999
+ },
+ "Parish of Saint Andrew": {
+ "lat": 13.2008061,
+ "lng": -61.2393885
+ },
+ "Grenadines": {
+ "lat": 13.0122965,
+ "lng": -61.2277301
+ },
+ "Parish of Charlotte": {
+ "lat": 13.2175451,
+ "lng": -61.1636244
+ },
+ "Parish of Saint David": {
+ "lat": 13.3154926,
+ "lng": -61.1985879
+ },
+ "": {
+ "lat": 12.984305,
+ "lng": -61.287228
+ }
+ },
+ "GD": {
+ "Saint Mark": {
+ "lat": 12.19023,
+ "lng": -61.6888738
+ },
+ "Saint George": {
+ "lat": 12.0560975,
+ "lng": -61.7487996
+ },
+ "Saint Patrick": {
+ "lat": 12.2056921,
+ "lng": -61.64211719999999
+ },
+ "Saint David": {
+ "lat": 12.0456435,
+ "lng": -61.6888738
+ },
+ "Carriacou and Petite Martinique": {
+ "lat": 12.4785888,
+ "lng": -61.4493842
+ },
+ "Saint Andrew": {
+ "lat": 12.1230598,
+ "lng": -61.64211719999999
+ },
+ "Saint John": {
+ "lat": 12.130824,
+ "lng": -61.71225699999999
+ },
+ "": {
+ "lat": 12.262776,
+ "lng": -61.604171
+ }
+ },
+ "BZ": {
+ "Orange Walk District": {
+ "lat": 17.760353,
+ "lng": -88.86469799999999
+ },
+ "Cayo District": {
+ "lat": 17.0984445,
+ "lng": -88.94138650000001
+ },
+ "Belize District": {
+ "lat": 17.5677679,
+ "lng": -88.4016041
+ },
+ "Toledo District": {
+ "lat": 16.2491923,
+ "lng": -88.86469799999999
+ },
+ "Stann Creek District": {
+ "lat": 16.8116631,
+ "lng": -88.4016041
+ },
+ "Corozal District": {
+ "lat": 18.1349238,
+ "lng": -88.24611829999999
+ },
+ "": {
+ "lat": 17.189877,
+ "lng": -88.49765
+ }
+ },
+ "SV": {
+ "Departamento de La Libertad": {
+ "lat": 13.6817661,
+ "lng": -89.3606298
+ },
+ "Departamento de La Paz": {
+ "lat": 13.495364,
+ "lng": -88.9796776
+ },
+ "Departamento de Usulutan": {
+ "lat": 13.4470634,
+ "lng": -88.55653099999999
+ },
+ "Departamento de Chalatenango": {
+ "lat": 14.1916648,
+ "lng": -89.1705998
+ },
+ "Departamento de San Salvador": {
+ "lat": 13.7739997,
+ "lng": -89.20867729999999
+ },
+ "Departamento de Sonsonate": {
+ "lat": 13.682358,
+ "lng": -89.6628111
+ },
+ "Departamento de San Vicente": {
+ "lat": 13.5868561,
+ "lng": -88.7493998
+ },
+ "Departamento de La Union": {
+ "lat": 13.4886443,
+ "lng": -87.89424509999999
+ },
+ "Departamento de Santa Ana": {
+ "lat": 14.1461121,
+ "lng": -89.5120084
+ },
+ "Departamento de San Miguel": {
+ "lat": 13.4792922,
+ "lng": -88.1779182
+ },
+ "Departamento de Cuscatlan": {
+ "lat": 13.8661957,
+ "lng": -89.0561532
+ },
+ "Departamento de Ahuachapan": {
+ "lat": 13.8216148,
+ "lng": -89.9253233
+ },
+ "Departamento de Morazan": {
+ "lat": 13.7682,
+ "lng": -88.1291387
+ },
+ "": {
+ "lat": 13.794185,
+ "lng": -88.89653
+ }
+ },
+ "GT": {
+ "Zacapa": {
+ "lat": 14.9676218,
+ "lng": -89.5356094
+ },
+ "Guatemala": {
+ "lat": 15.783471,
+ "lng": -90.23075899999999
+ },
+ "Totonicapán": {
+ "lat": 14.9173402,
+ "lng": -91.36139229999999
+ },
+ "Sololá": {
+ "lat": 14.7666085,
+ "lng": -91.17850159999999
+ },
+ "Santa Rosa Department": {
+ "lat": 14.1928003,
+ "lng": -90.37483540000001
+ },
+ "San Marcos": {
+ "lat": 14.9609782,
+ "lng": -91.80745859999999
+ },
+ "Sacatepéquez": {
+ "lat": 14.5178379,
+ "lng": -90.7152749
+ },
+ "Petén": {
+ "lat": 16.912033,
+ "lng": -90.2995785
+ },
+ "Baja Verapaz": {
+ "lat": 15.1255867,
+ "lng": -90.37483540000001
+ },
+ "Quiché": {
+ "lat": 15.4983808,
+ "lng": -90.9820668
+ },
+ "Quetzaltenango": {
+ "lat": 14.8446068,
+ "lng": -91.5231866
+ },
+ "Izabal Department": {
+ "lat": 15.4036847,
+ "lng": -89.1384441
+ },
+ "Alta Verapaz": {
+ "lat": 15.5942883,
+ "lng": -90.14949879999999
+ },
+ "Departamento de Escuintla": {
+ "lat": 14.1910912,
+ "lng": -90.9820668
+ },
+ "Suchitepeque": {
+ "lat": 14.4215982,
+ "lng": -91.4048249
+ },
+ "Departamento de Jutiapa": {
+ "lat": 14.1930802,
+ "lng": -89.9253233
+ },
+ "Jalapa": {
+ "lat": 14.6325483,
+ "lng": -89.9930052
+ },
+ "Chiquimula": {
+ "lat": 14.7924897,
+ "lng": -89.5450458
+ },
+ "Departamento de Huehuetenango": {
+ "lat": 15.5879914,
+ "lng": -91.67606909999999
+ },
+ "El Progreso": {
+ "lat": 14.9388732,
+ "lng": -90.0746767
+ },
+ "Chimaltenango": {
+ "lat": 14.6589734,
+ "lng": -90.8245242
+ },
+ "": {
+ "lat": 15.783471,
+ "lng": -90.230759
+ }
+ },
+ "HN": {
+ "Cortés Department": {
+ "lat": 15.4923508,
+ "lng": -88.0900762
+ },
+ "Yoro Department": {
+ "lat": 15.2949679,
+ "lng": -87.14228949999999
+ },
+ "Lempira Department": {
+ "lat": 14.1887698,
+ "lng": -88.55653099999999
+ },
+ "Santa Bárbara Department": {
+ "lat": 15.1202795,
+ "lng": -88.4016041
+ },
+ "Bay Islands": {
+ "lat": 16.4826614,
+ "lng": -85.8793252
+ },
+ "Colón Department": {
+ "lat": 15.6425965,
+ "lng": -85.52002399999999
+ },
+ "Atlántida Department": {
+ "lat": 15.6696283,
+ "lng": -87.14228949999999
+ },
+ "Francisco Morazán Department": {
+ "lat": 14.0650449,
+ "lng": -87.17150389999999
+ },
+ "Comayagua Department": {
+ "lat": 14.5534828,
+ "lng": -87.6186379
+ },
+ "Copán Department": {
+ "lat": 14.9360838,
+ "lng": -88.86469799999999
+ },
+ "La Paz Department": {
+ "lat": 13.9984833,
+ "lng": -87.9334803
+ },
+ "Choluteca Department": {
+ "lat": 13.3011523,
+ "lng": -87.1928293
+ },
+ "Valle Department": {
+ "lat": 13.5372374,
+ "lng": -87.4851655
+ },
+ "Olancho Department": {
+ "lat": 14.8067406,
+ "lng": -85.76666449999999
+ },
+ "Gracias a Dios Department": {
+ "lat": 15.341806,
+ "lng": -84.6060449
+ },
+ "Ocotepeque Department": {
+ "lat": 14.5170347,
+ "lng": -89.0561532
+ },
+ "Intibucá Department": {
+ "lat": 14.372734,
+ "lng": -88.24611829999999
+ },
+ "El Paraíso Department": {
+ "lat": 13.944485,
+ "lng": -86.85283659999999
+ },
+ "": {
+ "lat": 15.199999,
+ "lng": -86.241905
+ }
+ },
+ "NI": {
+ "Managua Department": {
+ "lat": 12.1853677,
+ "lng": -86.3782198
+ },
+ "Madriz Department": {
+ "lat": 13.4714521,
+ "lng": -86.3782198
+ },
+ "Departamento de Chinandega": {
+ "lat": 12.6982149,
+ "lng": -87.14228949999999
+ },
+ "Matagalpa Department": {
+ "lat": 12.9498436,
+ "lng": -85.4375574
+ },
+ "Departamento de Rivas": {
+ "lat": 11.3599685,
+ "lng": -85.8895551
+ },
+ "Masaya Department": {
+ "lat": 12.0896453,
+ "lng": -86.05296039999999
+ },
+ "León Department": {
+ "lat": 12.690563,
+ "lng": -86.49965460000001
+ },
+ "Chontales Department": {
+ "lat": 12.1216944,
+ "lng": -85.1894045
+ },
+ "Jinotega Department": {
+ "lat": 13.7810091,
+ "lng": -85.52002399999999
+ },
+ "Granada Department": {
+ "lat": 11.8151151,
+ "lng": -85.971322
+ },
+ "Estelí Department": {
+ "lat": 13.2424713,
+ "lng": -86.49965460000001
+ },
+ "Boaco Department": {
+ "lat": 12.492724,
+ "lng": -85.52002399999999
+ },
+ "": {
+ "lat": 12.865416,
+ "lng": -85.207229
+ }
+ },
+ "CR": {
+ "Alajuela Province": {
+ "lat": 10.391583,
+ "lng": -84.4382721
+ },
+ "Provincia de San Jose": {
+ "lat": 9.9280694,
+ "lng": -84.0907246
+ },
+ "Puntarenas Province": {
+ "lat": 9.2169531,
+ "lng": -83.33618799999999
+ },
+ "Heredia Province": {
+ "lat": 10.473523,
+ "lng": -84.01674229999999
+ },
+ "Cartago Province": {
+ "lat": 9.7539596,
+ "lng": -83.67739279999999
+ },
+ "Guanacaste Province": {
+ "lat": 10.4072992,
+ "lng": -85.4375574
+ },
+ "Limón Province": {
+ "lat": 10.1064393,
+ "lng": -83.5070203
+ },
+ "": {
+ "lat": 9.748917,
+ "lng": -83.753428
+ }
+ },
+ "EC": {
+ "Zamora Chinchipe": {
+ "lat": -4.0655892,
+ "lng": -78.9503525
+ },
+ "Provincia de El Oro": {
+ "lat": -3.2592413,
+ "lng": -79.9583541
+ },
+ "Provincia de Loja": {
+ "lat": -4.1635066,
+ "lng": -79.560344
+ },
+ "Provincia del Guayas": {
+ "lat": -1.9574839,
+ "lng": -79.9192702
+ },
+ "Provincia de Los Rios": {
+ "lat": -1.0230607,
+ "lng": -79.4608897
+ },
+ "Provincia de Imbabura": {
+ "lat": 0.3499768,
+ "lng": -78.12601289999999
+ },
+ "Tungurahua": {
+ "lat": -1.2635284,
+ "lng": -78.5660852
+ },
+ "Provincia de Pichincha": {
+ "lat": -0.1464847,
+ "lng": -78.4751945
+ },
+ "Provincia del Carchi": {
+ "lat": 0.5026912,
+ "lng": -77.9042521
+ },
+ "Provincia de Manabi": {
+ "lat": -1.0543434,
+ "lng": -80.45264399999999
+ },
+ "Provincia de Napo": {
+ "lat": -0.9955963999999998,
+ "lng": -77.8129684
+ },
+ "Canar": {
+ "lat": -2.555695,
+ "lng": -78.9344814
+ },
+ "Morona Santiago": {
+ "lat": -2.3051062,
+ "lng": -78.11468660000001
+ },
+ "Provincia del Azuay": {
+ "lat": -2.8943068,
+ "lng": -78.9968344
+ },
+ "Provincia de Sucumbios": {
+ "lat": 0.08892309999999999,
+ "lng": -76.8897557
+ },
+ "Pastaza": {
+ "lat": -1.4882265,
+ "lng": -78.00310569999999
+ },
+ "Provincia de Cotopaxi": {
+ "lat": -0.8384206,
+ "lng": -78.6662678
+ },
+ "Provincia de Santo Domingo de los Tsachilas": {
+ "lat": -0.2521882,
+ "lng": -79.1879383
+ },
+ "Provincia de Santa Elena": {
+ "lat": -2.2267105,
+ "lng": -80.859499
+ },
+ "Provincia de Esmeraldas": {
+ "lat": 0.3217414,
+ "lng": -79.468107
+ },
+ "Provincia de Bolivar": {
+ "lat": -1.7095828,
+ "lng": -79.0450429
+ },
+ "Chimborazo": {
+ "lat": -1.4693018,
+ "lng": -78.8169396
+ },
+ "Provincia de Galapagos": {
+ "lat": -0.9537690999999999,
+ "lng": -90.9656019
+ },
+ "Orellana": {
+ "lat": -0.4545163,
+ "lng": -76.9950286
+ },
+ "": {
+ "lat": -1.831239,
+ "lng": -78.183406
+ }
+ },
+ "CO": {
+ "Cundinamarca": {
+ "lat": 5.026002999999999,
+ "lng": -74.0300122
+ },
+ "Departamento del Valle del Cauca": {
+ "lat": 3.8008893,
+ "lng": -76.64127119999999
+ },
+ "Departamento del Huila": {
+ "lat": 2.5359349,
+ "lng": -75.52766989999999
+ },
+ "Departamento de Santander": {
+ "lat": 6.6437076,
+ "lng": -73.65362089999999
+ },
+ "Casanare Department": {
+ "lat": 5.7589269,
+ "lng": -71.5723953
+ },
+ "Antioquia": {
+ "lat": 6.2476598,
+ "lng": -75.565816
+ },
+ "Caldas Department": {
+ "lat": 5.29826,
+ "lng": -75.2479061
+ },
+ "Departamento del Meta": {
+ "lat": 3.2719904,
+ "lng": -73.087749
+ },
+ "Departamento del Cauca": {
+ "lat": 2.20849,
+ "lng": -77.8288199
+ },
+ "Norte de Santander Department": {
+ "lat": 7.9462831,
+ "lng": -72.8988069
+ },
+ "La Guajira Department": {
+ "lat": 11.3547743,
+ "lng": -72.5204827
+ },
+ "Atlántico": {
+ "lat": 10.6966159,
+ "lng": -74.8741045
+ },
+ "Departamento de Boyaca": {
+ "lat": 5.454511,
+ "lng": -73.362003
+ },
+ "Departamento del Putumayo": {
+ "lat": 0.4359506,
+ "lng": -75.52766989999999
+ },
+ "Departamento del Magdalena": {
+ "lat": 10.4113014,
+ "lng": -74.4056612
+ },
+ "Departamento de Sucre": {
+ "lat": 8.813977,
+ "lng": -74.723283
+ },
+ "Departamento del Cesar": {
+ "lat": 9.3372948,
+ "lng": -73.65362089999999
+ },
+ "Bogota D.C.": {
+ "lat": 4.710988599999999,
+ "lng": -74.072092
+ },
+ "Amazonas": {
+ "lat": -1.4429123,
+ "lng": -71.5723953
+ },
+ "Departamento de Bolivar": {
+ "lat": 8.6704382,
+ "lng": -74.0300122
+ },
+ "Departamento de Narino": {
+ "lat": 1.289151,
+ "lng": -77.35794000000001
+ },
+ "Departamento de Cordoba": {
+ "lat": 8.7509716,
+ "lng": -75.8785438
+ },
+ "Departamento de Arauca": {
+ "lat": 6.547306,
+ "lng": -71.0022311
+ },
+ "Departamento del Choco": {
+ "lat": 5.6914548,
+ "lng": -76.65897939999999
+ },
+ "Departamento de Risaralda": {
+ "lat": 5.315847499999999,
+ "lng": -75.9927652
+ },
+ "Departamento de Tolima": {
+ "lat": 4.092516799999999,
+ "lng": -75.1545381
+ },
+ "Departamento del Caqueta": {
+ "lat": 0.869892,
+ "lng": -73.8419063
+ },
+ "Guainía Department": {
+ "lat": 2.585393,
+ "lng": -68.52471489999999
+ },
+ "Departamento del Vichada": {
+ "lat": 4.4234452,
+ "lng": -69.2877535
+ },
+ "Quindio Department": {
+ "lat": 4.4610191,
+ "lng": -75.667356
+ },
+ "Departamento del Vaupes": {
+ "lat": 0.8553561,
+ "lng": -70.81199529999999
+ },
+ "Departamento del Guaviare": {
+ "lat": 2.043924000000001,
+ "lng": -72.331113
+ },
+ "": {
+ "lat": 4.570868,
+ "lng": -74.297333
+ }
+ },
+ "PE": {
+ "Loreto": {
+ "lat": -4.232472899999999,
+ "lng": -74.21793260000001
+ },
+ "Ancash": {
+ "lat": -9.3250497,
+ "lng": -77.5619419
+ },
+ "La Libertad": {
+ "lat": -8.143593300000001,
+ "lng": -78.4751945
+ },
+ "Tumbes": {
+ "lat": -3.5564921,
+ "lng": -80.4270885
+ },
+ "Region de Huanuco": {
+ "lat": -9.4505511,
+ "lng": -76.2710833
+ },
+ "Region de San Martin": {
+ "lat": -7.244488100000001,
+ "lng": -76.8259652
+ },
+ "Piura": {
+ "lat": -5.1782884,
+ "lng": -80.6548882
+ },
+ "Lambayeque": {
+ "lat": -6.7044468,
+ "lng": -79.9042472
+ },
+ "Ucayali": {
+ "lat": -9.8251183,
+ "lng": -73.087749
+ },
+ "Amazonas": {
+ "lat": -5.115146,
+ "lng": -78.11082789999999
+ },
+ "Cajamarca": {
+ "lat": -7.1617465,
+ "lng": -78.51278549999999
+ },
+ "Lima region": {
+ "lat": -12.2720956,
+ "lng": -76.2710833
+ },
+ "Callao": {
+ "lat": -12.0508491,
+ "lng": -77.1259843
+ },
+ "Cusco": {
+ "lat": -13.53195,
+ "lng": -71.96746259999999
+ },
+ "Pasco": {
+ "lat": -10.4475753,
+ "lng": -75.1545381
+ },
+ "Junin": {
+ "lat": -11.1581925,
+ "lng": -75.9926306
+ },
+ "Puno": {
+ "lat": -15.8402218,
+ "lng": -70.0218805
+ },
+ "Tacna": {
+ "lat": -18.0067602,
+ "lng": -70.2460246
+ },
+ "Ica": {
+ "lat": -14.07546,
+ "lng": -75.7341811
+ },
+ "Arequipa": {
+ "lat": -16.4090474,
+ "lng": -71.53745099999999
+ },
+ "Departamento de Moquegua": {
+ "lat": -16.9428373,
+ "lng": -71.0022311
+ },
+ "Huancavelica": {
+ "lat": -12.7861978,
+ "lng": -74.9764024
+ },
+ "Madre de Dios": {
+ "lat": -11.7668705,
+ "lng": -70.81199529999999
+ },
+ "Lima": {
+ "lat": -12.0463731,
+ "lng": -77.042754
+ },
+ "Ayacucho": {
+ "lat": -13.1638737,
+ "lng": -74.22356409999999
+ },
+ "Region de Apurimac": {
+ "lat": -14.0504533,
+ "lng": -73.087749
+ },
+ "": {
+ "lat": -9.189967,
+ "lng": -75.015152
+ }
+ },
+ "PA": {
+ "Provincia de Herrera": {
+ "lat": 7.7704282,
+ "lng": -80.7214417
+ },
+ "Provincia de Veraguas": {
+ "lat": 8.1231033,
+ "lng": -81.0754657
+ },
+ "Panamá Oeste Province": {
+ "lat": 8.791318,
+ "lng": -80.0087746
+ },
+ "Provincia del Darien": {
+ "lat": 7.868171299999999,
+ "lng": -77.8367282
+ },
+ "Provincia de Panama": {
+ "lat": 9.1088003,
+ "lng": -78.9288242
+ },
+ "Provincia de Colon": {
+ "lat": 9.1851989,
+ "lng": -80.0534923
+ },
+ "Guna Yala": {
+ "lat": 9.238864999999999,
+ "lng": -78.2248291
+ },
+ "Provincia de Cocle": {
+ "lat": 8.400144899999999,
+ "lng": -80.42152460000001
+ },
+ "Bocas del Toro Province": {
+ "lat": 9.293417900000001,
+ "lng": -82.7347142
+ },
+ "Ngoebe-Bugle": {
+ "lat": 8.6595833,
+ "lng": -81.77870209999999
+ },
+ "Chiriquí Province": {
+ "lat": 8.3866964,
+ "lng": -82.2800546
+ },
+ "Embera-Wounaan": {
+ "lat": 8.3766983,
+ "lng": -77.6536125
+ },
+ "": {
+ "lat": 8.537981,
+ "lng": -80.782127
+ }
+ },
+ "HT": {
+ "Sud": {
+ "lat": 18.3320005,
+ "lng": -73.7007088
+ },
+ "Departement de l'Artibonite": {
+ "lat": 19.362902,
+ "lng": -72.4258145
+ },
+ "Departement de l'Ouest": {
+ "lat": 18.4957015,
+ "lng": -72.4731529
+ },
+ "Departement du Nord-Est": {
+ "lat": 19.4889723,
+ "lng": -71.8571331
+ },
+ "Nord-Ouest": {
+ "lat": 19.8374009,
+ "lng": -73.0405277
+ },
+ "Centre": {
+ "lat": 18.9582742,
+ "lng": -72.0468164
+ },
+ "Departement de Nippes": {
+ "lat": 18.3990735,
+ "lng": -73.4180211
+ },
+ "Grand'Anse": {
+ "lat": 18.5489259,
+ "lng": -74.07701
+ },
+ "Nord": {
+ "lat": 19.5687715,
+ "lng": -72.189
+ },
+ "": {
+ "lat": 18.971187,
+ "lng": -72.285215
+ }
+ },
+ "CL": {
+ "Region de Valparaiso": {
+ "lat": -32.5040172,
+ "lng": -71.0022311
+ },
+ "Coquimbo Region": {
+ "lat": -30.540181,
+ "lng": -70.81199529999999
+ },
+ "Region del Biobio": {
+ "lat": -37.4464428,
+ "lng": -72.1416132
+ },
+ "Maule Region": {
+ "lat": -35.5163603,
+ "lng": -71.5723953
+ },
+ "O'Higgins Region": {
+ "lat": -34.5755374,
+ "lng": -71.0022311
+ },
+ "Santiago Metropolitan": {
+ "lat": -33.4843354,
+ "lng": -70.6216794
+ },
+ "Region de la Araucania": {
+ "lat": -38.948921,
+ "lng": -72.331113
+ },
+ "Atacama": {
+ "lat": -23.8634189,
+ "lng": -69.1328491
+ },
+ "Los Ríos Region": {
+ "lat": -40.2310217,
+ "lng": -72.331113
+ },
+ "Antofagasta": {
+ "lat": -23.6509279,
+ "lng": -70.39750219999999
+ },
+ "Ñuble": {
+ "lat": -36.7225743,
+ "lng": -71.7622481
+ },
+ "Region of Magallanes": {
+ "lat": -53.4428344,
+ "lng": -72.1496817
+ },
+ "Los Lagos Region": {
+ "lat": -41.9197779,
+ "lng": -72.1416132
+ },
+ "Region de Arica y Parinacota": {
+ "lat": -18.5940485,
+ "lng": -69.4784541
+ },
+ "Aysén": {
+ "lat": -45.4037315,
+ "lng": -72.6864919
+ },
+ "Tarapacá": {
+ "lat": -19.9243492,
+ "lng": -69.5109513
+ },
+ "": {
+ "lat": -35.675147,
+ "lng": -71.542969
+ }
+ },
+ "BO": {
+ "Tarija Department": {
+ "lat": -21.5831595,
+ "lng": -63.95861110000001
+ },
+ "Beni Department": {
+ "lat": -14.3782747,
+ "lng": -65.0957792
+ },
+ "Chuquisaca Department": {
+ "lat": -20.0249144,
+ "lng": -64.1478236
+ },
+ "Santa Cruz Department": {
+ "lat": -16.7476037,
+ "lng": -62.0750998
+ },
+ "Departamento de Cochabamba": {
+ "lat": -17.5681675,
+ "lng": -65.475736
+ },
+ "Potosí Department": {
+ "lat": -20.624713,
+ "lng": -66.9988011
+ },
+ "Departamento de Pando": {
+ "lat": -10.7988901,
+ "lng": -66.9988011
+ },
+ "Oruro": {
+ "lat": -17.9716723,
+ "lng": -67.0931378
+ },
+ "La Paz Department": {
+ "lat": -15.0892416,
+ "lng": -68.52471489999999
+ },
+ "": {
+ "lat": -16.290154,
+ "lng": -63.588653
+ }
+ },
+ "PF": {
+ "Iles Marquises": {
+ "lat": -9.781216200000001,
+ "lng": -139.0817124
+ },
+ "Iles Tuamotu-Gambier": {
+ "lat": -18.033333,
+ "lng": -141.416667
+ },
+ "Leeward Islands": {
+ "lat": -16.39540465,
+ "lng": -152.7870564
+ },
+ "Iles du Vent": {
+ "lat": -17.53799845,
+ "lng": -149.41834955
+ },
+ "": {
+ "lat": -17.679742,
+ "lng": -149.406843
+ }
+ },
+ "TK": {
+ "Fakaofo": {
+ "lat": -9.380255499999999,
+ "lng": -171.2188355
+ },
+ "Nukunonu": {
+ "lat": -9.1977966,
+ "lng": -171.8507811
+ },
+ "Atafu": {
+ "lat": -8.55451154047545,
+ "lng": -172.4955714262993
+ },
+ "": {
+ "lat": -8.967363,
+ "lng": -171.855881
+ }
+ },
+ "TO": {
+ "Tongatapu": {
+ "lat": -21.1465968,
+ "lng": -175.2515482
+ },
+ "ʻEua": {
+ "lat": -21.3869753,
+ "lng": -174.9297206
+ },
+ "Vava'u": {
+ "lat": -18.622756,
+ "lng": -173.9902982
+ },
+ "Niuas": {
+ "lat": -15.9594,
+ "lng": -173.783
+ },
+ "Ha'apai": {
+ "lat": -19.75,
+ "lng": -174.366667
+ },
+ "": {
+ "lat": -21.178986,
+ "lng": -175.198242
+ }
+ },
+ "WF": {
+ "Uvea": {
+ "lat": -13.2959105,
+ "lng": -176.2056843
+ },
+ "Sigave": {
+ "lat": -14.2968356,
+ "lng": -178.1581081
+ },
+ "Alo": {
+ "lat": -14.2903576,
+ "lng": -178.1193763
+ },
+ "": {
+ "lat": -13.768752,
+ "lng": -177.156097
+ }
+ },
+ "WS": {
+ "Va'a-o-Fonoti": {
+ "lat": -13.9470903,
+ "lng": -171.5431872
+ },
+ "Palauli": {
+ "lat": -13.7585016,
+ "lng": -172.299737
+ },
+ "Satupa'itea": {
+ "lat": -13.7731169,
+ "lng": -172.3302677
+ },
+ "Fa'asaleleaga": {
+ "lat": -13.6307638,
+ "lng": -172.2365981
+ },
+ "Gagaifomauga": {
+ "lat": -13.5468007,
+ "lng": -172.4969331
+ },
+ "Gaga'emauga": {
+ "lat": -13.5428666,
+ "lng": -172.366887
+ },
+ "Aiga-i-le-Tai": {
+ "lat": -13.8513791,
+ "lng": -172.0325401
+ },
+ "Atua": {
+ "lat": -13.9787053,
+ "lng": -171.6254283
+ },
+ "A'ana": {
+ "lat": -13.898418,
+ "lng": -171.9752995
+ },
+ "Tuamasaga": {
+ "lat": -13.9163592,
+ "lng": -171.8224362
+ },
+ "Vaisigano": {
+ "lat": -13.5413827,
+ "lng": -172.7023383
+ },
+ "": {
+ "lat": -13.759029,
+ "lng": -172.104629
+ }
+ },
+ "MP": {
+ "Rota": {
+ "lat": 14.1508752,
+ "lng": 145.2148736
+ },
+ "Tinian": {
+ "lat": 15.0043455,
+ "lng": 145.6356577
+ },
+ "Northern Islands": {
+ "lat": 18.27354396328492,
+ "lng": 145.4797085491793
+ },
+ "Saipan": {
+ "lat": 15.1850483,
+ "lng": 145.7467259
+ },
+ "": {
+ "lat": 17.33083,
+ "lng": 145.38469
+ }
+ },
+ "UM": {
+ "Wake Island": {
+ "lat": 19.279619,
+ "lng": 166.6499348
+ },
+ "": {
+ "lat": 0,
+ "lng": 0
+ }
+ },
+ "US": {
+ "Texas": {
+ "lat": 31.9685988,
+ "lng": -99.9018131
+ },
+ "Alabama": {
+ "lat": 32.3182314,
+ "lng": -86.902298
+ },
+ "Virginia": {
+ "lat": 37.4315734,
+ "lng": -78.6568942
+ },
+ "West Virginia": {
+ "lat": 38.5976262,
+ "lng": -80.4549026
+ },
+ "Arkansas": {
+ "lat": 35.20105,
+ "lng": -91.8318334
+ },
+ "Delaware": {
+ "lat": 38.9108325,
+ "lng": -75.52766989999999
+ },
+ "Florida": {
+ "lat": 27.6648274,
+ "lng": -81.5157535
+ },
+ "Georgia": {
+ "lat": 32.1574351,
+ "lng": -82.90712300000001
+ },
+ "Illinois": {
+ "lat": 40.6331249,
+ "lng": -89.3985283
+ },
+ "Indiana": {
+ "lat": 40.5512165,
+ "lng": -85.60236429999999
+ },
+ "Maryland": {
+ "lat": 39.0457549,
+ "lng": -76.64127119999999
+ },
+ "Kansas": {
+ "lat": 39.011902,
+ "lng": -98.4842465
+ },
+ "Kentucky": {
+ "lat": 37.8393332,
+ "lng": -84.2700179
+ },
+ "Missouri": {
+ "lat": 37.9642529,
+ "lng": -91.8318334
+ },
+ "North Carolina": {
+ "lat": 35.7595731,
+ "lng": -79.01929969999999
+ },
+ "Ohio": {
+ "lat": 40.4172871,
+ "lng": -82.90712300000001
+ },
+ "Oklahoma": {
+ "lat": 35.0077519,
+ "lng": -97.092877
+ },
+ "South Carolina": {
+ "lat": 33.836081,
+ "lng": -81.1637245
+ },
+ "Tennessee": {
+ "lat": 35.5174913,
+ "lng": -86.5804473
+ },
+ "District of Columbia": {
+ "lat": 38.9071923,
+ "lng": -77.0368707
+ },
+ "Louisiana": {
+ "lat": 30.5190775,
+ "lng": -91.5208624
+ },
+ "Mississippi": {
+ "lat": 32.3546679,
+ "lng": -89.3985283
+ },
+ "New Jersey": {
+ "lat": 40.0583238,
+ "lng": -74.4056612
+ },
+ "Pennsylvania": {
+ "lat": 41.2033216,
+ "lng": -77.1945247
+ },
+ "Connecticut": {
+ "lat": 41.6032207,
+ "lng": -73.087749
+ },
+ "Iowa": {
+ "lat": 41.8780025,
+ "lng": -93.097702
+ },
+ "Massachusetts": {
+ "lat": 42.4072107,
+ "lng": -71.3824374
+ },
+ "Maine": {
+ "lat": 45.253783,
+ "lng": -69.4454689
+ },
+ "Michigan": {
+ "lat": 44.3148443,
+ "lng": -85.60236429999999
+ },
+ "Minnesota": {
+ "lat": 46.729553,
+ "lng": -94.6858998
+ },
+ "Nebraska": {
+ "lat": 41.4925374,
+ "lng": -99.9018131
+ },
+ "New York": {
+ "lat": 40.7127753,
+ "lng": -74.0059728
+ },
+ "South Dakota": {
+ "lat": 43.9695148,
+ "lng": -99.9018131
+ },
+ "Wisconsin": {
+ "lat": 43.7844397,
+ "lng": -88.7878678
+ },
+ "North Dakota": {
+ "lat": 47.5514926,
+ "lng": -101.0020119
+ },
+ "New Hampshire": {
+ "lat": 43.1938516,
+ "lng": -71.5723953
+ },
+ "Rhode Island": {
+ "lat": 41.5800945,
+ "lng": -71.4774291
+ },
+ "Vermont": {
+ "lat": 44.5588028,
+ "lng": -72.57784149999999
+ },
+ "Arizona": {
+ "lat": 34.0489281,
+ "lng": -111.0937311
+ },
+ "California": {
+ "lat": 36.778261,
+ "lng": -119.4179324
+ },
+ "New Mexico": {
+ "lat": 34.9727305,
+ "lng": -105.0323635
+ },
+ "Utah": {
+ "lat": 39.3209801,
+ "lng": -111.0937311
+ },
+ "Colorado": {
+ "lat": 39.5500507,
+ "lng": -105.7820674
+ },
+ "Nevada": {
+ "lat": 38.8026097,
+ "lng": -116.419389
+ },
+ "Idaho": {
+ "lat": 44.0682019,
+ "lng": -114.7420408
+ },
+ "Alaska": {
+ "lat": 63.588753,
+ "lng": -154.4930619
+ },
+ "Montana": {
+ "lat": 46.8796822,
+ "lng": -110.3625658
+ },
+ "Oregon": {
+ "lat": 43.8041334,
+ "lng": -120.5542012
+ },
+ "Washington": {
+ "lat": 47.7510741,
+ "lng": -120.7401386
+ },
+ "Wyoming": {
+ "lat": 43.0759678,
+ "lng": -107.2902839
+ },
+ "Hawaii": {
+ "lat": 19.8986819,
+ "lng": -155.6658568
+ },
+ "": {
+ "lat": 37.09024,
+ "lng": -95.712891
+ }
+ },
+ "VI": {
+ "Saint Thomas Island": {
+ "lat": 18.3380965,
+ "lng": -64.8940946
+ },
+ "Saint Croix Island": {
+ "lat": 17.7245968,
+ "lng": -64.83479919999999
+ },
+ "Saint John Island": {
+ "lat": 18.3368114,
+ "lng": -64.7280952
+ },
+ "": {
+ "lat": 18.335765,
+ "lng": -64.896335
+ }
+ },
+ "AS": {
+ "Western District": {
+ "lat": -14.3249786,
+ "lng": -170.7214561
+ },
+ "Manu'a District": {
+ "lat": -14.2302774,
+ "lng": -169.5154102
+ },
+ "Swains Island": {
+ "lat": -11.0552207,
+ "lng": -171.0782253
+ },
+ "Eastern District": {
+ "lat": -14.273298,
+ "lng": -170.7030625
+ },
+ "Rose Island": {
+ "lat": -14.5212082,
+ "lng": -168.1792434
+ },
+ "": {
+ "lat": -14.270972,
+ "lng": -170.132217
+ }
+ },
+ "CA": {
+ "British Columbia": {
+ "lat": 53.7266683,
+ "lng": -127.6476206
+ },
+ "Nova Scotia": {
+ "lat": 45.07784729999999,
+ "lng": -63.5466822
+ },
+ "Saskatchewan": {
+ "lat": 52.9399159,
+ "lng": -106.4508639
+ },
+ "Alberta": {
+ "lat": 53.9332706,
+ "lng": -116.5765035
+ },
+ "New Brunswick": {
+ "lat": 46.5653163,
+ "lng": -66.46191639999999
+ },
+ "Ontario": {
+ "lat": 51.253775,
+ "lng": -85.3232139
+ },
+ "Quebec": {
+ "lat": 46.8130816,
+ "lng": -71.20745959999999
+ },
+ "Prince Edward Island": {
+ "lat": 46.3188025,
+ "lng": -63.14610099999999
+ },
+ "Manitoba": {
+ "lat": 53.7608608,
+ "lng": -98.81387629999999
+ },
+ "Nunavut": {
+ "lat": 70.2997711,
+ "lng": -83.1075769
+ },
+ "Newfoundland and Labrador": {
+ "lat": 53.49436391800336,
+ "lng": -60.22052574294098
+ },
+ "Yukon": {
+ "lat": 64.2823274,
+ "lng": -135
+ },
+ "Northwest Territories": {
+ "lat": 63.588753,
+ "lng": -115.5069381
+ },
+ "": {
+ "lat": 56.130366,
+ "lng": -106.346771
+ }
+ },
+ "SG": {
+ "": {
+ "lat": 1.3214412,
+ "lng": 103.7954157
+ }
+ }
+}
diff --git a/etc/geoip/iso3166-2.json b/etc/geoip/iso3166-2.json
new file mode 100755
index 0000000..1dbc92a
--- /dev/null
+++ b/etc/geoip/iso3166-2.json
@@ -0,0 +1,3411 @@
+{
+ "IR-02": "Māzandarān",
+ "CY-02": "Limassol District",
+ "IR-28": "North Khorasan",
+ "IR-26": "Qazvin Province",
+ "IR-06": "Khuzestan",
+ "CY-04": "Ammochostos",
+ "IR-04": "West Azerbaijan Province",
+ "IR-05": "Kermanshah Province",
+ "IR-01": "Gilan Province",
+ "IR-03": "East Azerbaijan Province",
+ "IR-13": "Hamadan Province",
+ "IR-27": "Golestan",
+ "IR-00": "Markazi",
+ "IR-23": "Tehran",
+ "IR-08": "Kerman",
+ "SO-BK": "Bakool",
+ "SO-BN": "Banaadir",
+ "SO-SH": "Lower Shabeelle",
+ "SO-WO": "Woqooyi Galbeed",
+ "SO-JD": "Middle Juba",
+ "SO-NU": "Nugaal",
+ "SO-GE": "Gedo",
+ "SO-MU": "Mudug",
+ "SO-TO": "Togdheer",
+ "SO-BR": "Bari",
+ "SO-AW": "Awdal",
+ "SO-HI": "Hiiraan",
+ "SO-BY": "Bay",
+ "IR-17": "Kohgiluyeh and Boyer-Ahmad Province",
+ "YE-AB": "Abyan Governorate",
+ "YE-SN": "Sanaa Governorate",
+ "YE-HD": "Muhafazat Hadramaout",
+ "YE-SA": "Amanat Alasimah",
+ "YE-BA": "Al Bayda",
+ "YE-RA": "Raymah",
+ "YE-LA": "Laḩij",
+ "YE-IB": "Ibb Governorate",
+ "YE-HJ": "Ḩajjah",
+ "YE-SU": "Soqatra",
+ "YE-DH": "Dhamār",
+ "YE-AM": "Omran",
+ "YE-JA": "Al Jawf",
+ "YE-SH": "Shabwah",
+ "YE-HU": "Al Hudaydah",
+ "YE-TA": "Ta‘izz",
+ "YE-MW": "Al Mahwit Governorate",
+ "YE-MR": "Al Mahrah Governorate",
+ "YE-DA": "Aḑ Ḑāli‘",
+ "LY-BU": "Al Butnan",
+ "LY-JA": "Al Jabal al Akhdar",
+ "LY-DR": "Darnah",
+ "LY-BA": "Sha'biyat Banghazi",
+ "LY-KF": "Al Kufrah",
+ "LY-MJ": "Al Marj",
+ "LY-WA": "Al Wahat",
+ "IQ-BA": "Basra",
+ "IQ-DA": "Duhok",
+ "IQ-SD": "Salah ad Din",
+ "IQ-SU": "Sulaymaniyah",
+ "IQ-MA": "Maysan",
+ "IQ-KI": "Kirkuk",
+ "IQ-KA": "Muhafazat Karbala'",
+ "IQ-AR": "Erbil",
+ "IQ-DI": "Diyālá",
+ "IQ-BG": "Baghdad",
+ "IQ-WA": "Muhafazat Wasit",
+ "IQ-AN": "Al Anbar",
+ "IQ-DQ": "Dhi Qar",
+ "IQ-NA": "An Najaf",
+ "IQ-NI": "Nineveh",
+ "IQ-BB": "Muhafazat Babil",
+ "IQ-QA": "Muhafazat al Qadisiyah",
+ "SA-03": "Medina Region",
+ "SA-05": "Al-Qassim Region",
+ "SA-01": "Riyadh Region",
+ "SA-07": "Tabuk Region",
+ "SA-04": "Eastern Province",
+ "SA-14": "'Asir Region",
+ "SA-12": "Al Jawf Region",
+ "SA-09": "Jazan Region",
+ "SA-08": "Northern Borders Region",
+ "SA-02": "Mecca Region",
+ "SA-10": "Najran Region",
+ "SA-06": "Ha'il Region",
+ "SA-11": "Al Bahah Region",
+ "IR-15": "Lorestan Province",
+ "IR-19": "Zanjan",
+ "IR-21": "Yazd Province",
+ "IR-09": "Razavi Khorasan",
+ "IR-30": "Alborz Province",
+ "IR-07": "Fars",
+ "IR-22": "Hormozgan",
+ "IR-14": "Chaharmahal and Bakhtiari Province",
+ "IR-18": "Bushehr Province",
+ "IR-20": "Semnan Province",
+ "IR-12": "Kurdistan Province",
+ "IR-24": "Ardabil Province",
+ "IR-25": "Qom Province",
+ "IR-10": "Isfahan",
+ "IR-16": "Ilam Province",
+ "IR-29": "South Khorasan Province",
+ "AO-LSU": "Lunda Sul",
+ "AO-LNO": "Luanda Norte",
+ "CY-01": "Nicosia",
+ "CY-03": "Larnaka",
+ "CY-05": "Pafos",
+ "CY-06": "Keryneia",
+ "AZ-NX": "Nakhichevan",
+ "AZ-LAN": "Lankaran Rayon",
+ "AZ-AST": "Astara",
+ "TZ-15": "Zanzibar Urban/West",
+ "TZ-06": "Pemba North",
+ "TZ-27": "Geita",
+ "TZ-01": "Arusha",
+ "TZ-25": "Tanga",
+ "TZ-24": "Tabora",
+ "TZ-20": "Rukwa",
+ "TZ-30": "Simiyu",
+ "TZ-22": "Shinyanga",
+ "TZ-02": "Dar es Salaam Region",
+ "TZ-29": "Njombe",
+ "TZ-18": "Mwanza",
+ "TZ-13": "Mara",
+ "TZ-28": "Katavi",
+ "TZ-09": "Kilimanjaro",
+ "TZ-16": "Morogoro",
+ "TZ-14": "Mbeya",
+ "TZ-04": "Iringa",
+ "TZ-08": "Kigoma",
+ "TZ-19": "Pwani",
+ "TZ-07": "Zanzibar North",
+ "TZ-03": "Dodoma",
+ "TZ-10": "Pemba South",
+ "TZ-05": "Kagera",
+ "TZ-26": "Manyara",
+ "TM-B": "Balkan",
+ "TM-S": "Ashgabat",
+ "TM-A": "Ahal",
+ "IL-Z": "Northern District",
+ "SY-ID": "Idlib Governorate",
+ "SY-HI": "Homs Governorate",
+ "SY-HL": "Aleppo Governorate",
+ "SY-DI": "Damascus Governorate",
+ "SY-LA": "Latakia Governorate",
+ "SY-SU": "As-Suwayda Governorate",
+ "AM-VD": "Vayots Dzor",
+ "AM-AR": "Ararat",
+ "AM-SU": "Syunik",
+ "ZM-04": "Luapula Province",
+ "KE-46": "Wajir",
+ "KE-11": "Kakamega",
+ "KE-30": "Nairobi",
+ "KE-13": "Kiambu",
+ "KE-36": "Nyeri",
+ "KE-29": "Murang'A",
+ "KE-38": "Siaya",
+ "KE-10": "Kajiado",
+ "KE-33": "Narok",
+ "KE-20": "Laikipia",
+ "KE-32": "Nandi",
+ "KE-31": "Nakuru",
+ "KE-22": "Machakos",
+ "KE-14": "Kilifi",
+ "KE-28": "Mombasa",
+ "KE-25": "Marsabit",
+ "KE-27": "Migori",
+ "KE-26": "Meru",
+ "KE-45": "Vihiga",
+ "KE-37": "Samburu",
+ "KE-24": "Mandera District",
+ "KE-43": "Turkana",
+ "KE-12": "Kericho",
+ "KE-21": "Lamu",
+ "KE-19": "Kwale",
+ "KE-18": "Kitui",
+ "KE-42": "Trans Nzoia",
+ "KE-17": "Kisumu",
+ "KE-16": "Kisii",
+ "KE-15": "Kirinyaga",
+ "KE-34": "Nyamira",
+ "KE-05": "Marakwet District",
+ "KE-47": "West Pokot District",
+ "KE-01": "Baringo",
+ "KE-09": "Isiolo",
+ "KE-08": "Homa Bay",
+ "KE-40": "Tana River District",
+ "KE-35": "Nyandarua",
+ "KE-07": "Garissa",
+ "KE-06": "Embu",
+ "KE-44": "Uasin Gishu",
+ "KE-41": "Tharaka - Nithi",
+ "KE-03": "Bungoma",
+ "RW-03": "Northern Province",
+ "RW-01": "Kigali",
+ "RW-04": "Western Province",
+ "CD-TO": "Tshopo",
+ "CD-SU": "Sud-Ubangi",
+ "CD-HL": "Haut-Lomami",
+ "CD-LO": "Lomami",
+ "CD-KE": "Kasaï-Oriental",
+ "CD-MO": "Mongala",
+ "CD-KS": "Kasai",
+ "CD-SA": "Sankuru",
+ "CD-MA": "Maniema",
+ "CD-TA": "Tanganyika",
+ "CD-HU": "Haut-Uele",
+ "CD-NK": "Nord Kivu",
+ "CD-NU": "Nord-Ubangi",
+ "CD-IT": "Ituri",
+ "CD-KC": "Kasai-Central",
+ "CD-SK": "South Kivu Province",
+ "CD-TU": "Tshuapa",
+ "DJ-TA": "Tadjourah",
+ "DJ-OB": "Obock",
+ "DJ-DJ": "Djibouti",
+ "DJ-DI": "Dikhil",
+ "DJ-AS": "Ali Sabieh Region",
+ "DJ-AR": "Arta Region",
+ "UG-C": "Central Region",
+ "UG-W": "Western Region",
+ "UG-E": "Eastern Region",
+ "UG-N": "Northern Region",
+ "CF-HM": "Haut-Mbomou",
+ "CF-MB": "Mbomou",
+ "CF-VK": "Vakaga",
+ "CF-HK": "Haute-Kotto",
+ "CF-BB": "Bamingui-Bangoran",
+ "CF-BK": "Basse-Kotto",
+ "CF-UK": "Ouaka",
+ "SC-16": "English River",
+ "SC-23": "Takamaka",
+ "SC-21": "Port Glaud",
+ "SC-19": "Plaisance",
+ "SC-12": "Glacis",
+ "SC-11": "Cascade",
+ "SC-08": "Beau Vallon",
+ "SC-05": "Anse Royale",
+ "TD-SI": "Sila",
+ "TD-EO": "Ennedi-Ouest",
+ "TD-WF": "Wadi Fira Region",
+ "TD-SA": "Salamat Region",
+ "TD-OD": "Ouadaï",
+ "JO-MN": "Ma’an",
+ "JO-JA": "Jerash",
+ "JO-AM": "Amman Governorate",
+ "JO-MD": "Madaba",
+ "JO-IR": "Irbid",
+ "JO-AJ": "Ajloun",
+ "JO-AT": "Tafielah",
+ "JO-AZ": "Zarqa",
+ "JO-BA": "Balqa",
+ "JO-KA": "Karak",
+ "JO-MA": "Mafraq",
+ "JO-AQ": "Aqaba",
+ "GR-F": "Ionian Islands",
+ "GR-G": "West Greece",
+ "GR-J": "Peloponnese",
+ "GR-E": "Thessaly",
+ "GR-L": "South Aegean",
+ "GR-I": "Attica",
+ "GR-D": "Epirus",
+ "GR-H": "Central Greece",
+ "GR-M": "Crete",
+ "GR-B": "Central Macedonia",
+ "GR-K": "North Aegean",
+ "GR-C": "West Macedonia",
+ "LB-BI": "Mohafazat Beqaa",
+ "LB-AS": "Mohafazat Liban-Nord",
+ "LB-JL": "Mohafazat Mont-Liban",
+ "LB-JA": "South Governorate",
+ "LB-NA": "Mohafazat Nabatiye",
+ "LB-AK": "Mohafazat Aakkar",
+ "LB-BA": "Beyrouth",
+ "LB-BH": "Mohafazat Baalbek-Hermel",
+ "PS-RFH": "Rafah Governorate",
+ "PS-DEB": "Deir al-Balah Governorate",
+ "PS-KYS": "Khan Yunis Governorate",
+ "PS-NGZ": "North Gaza Governorate",
+ "PS-GZA": "Gaza Governorate",
+ "IL-JM": "Jerusalem",
+ "PS-TKM": "Tulkarm Governorate",
+ "PS-TBS": "Tubas Governorate",
+ "PS-SLT": "Salfit Governorate",
+ "PS-RBH": "Ramallah and al-Bireh Governorate",
+ "PS-QQA": "Qalqilya Governorate",
+ "PS-NBS": "Nablus Governorate",
+ "PS-JEN": "Jenin Governorate",
+ "IL-M": "Central District",
+ "PS-HBN": "Hebron",
+ "PS-BTH": "Bethlehem Governorate",
+ "PS-JRH": "Jericho Governorate",
+ "KW-JA": "Muhafazat al Jahra'",
+ "KW-KU": "Al Asimah",
+ "KW-HA": "Hawalli",
+ "KW-AH": "Al Aḩmadī",
+ "KW-MU": "Mubārak al Kabīr",
+ "KW-FA": "Al Farwaniyah",
+ "OM-MA": "Muscat",
+ "OM-SJ": "Southeastern Governorate",
+ "OM-BS": "Al Batinah North Governorate",
+ "OM-DA": "Ad Dakhiliyah",
+ "OM-WU": "Al Wusta Governorate",
+ "OM-SS": "Northeastern Governorate",
+ "OM-ZU": "Dhofar",
+ "OM-MU": "Musandam Governorate",
+ "OM-ZA": "Ad Dhahirah",
+ "OM-BJ": "Al Batinah South",
+ "OM-BU": "Al Buraimi",
+ "QA-US": "Baladiyat Umm Salal",
+ "QA-WA": "Al Wakrah",
+ "QA-SH": "Al-Shahaniya",
+ "QA-MS": "Baladiyat ash Shamal",
+ "QA-RA": "Baladiyat ar Rayyan",
+ "QA-KH": "Al Khor",
+ "QA-DA": "Baladiyat ad Dawhah",
+ "BH-13": "Manama",
+ "BH-17": "Northern",
+ "BH-15": "Muharraq",
+ "BH-14": "Southern Governorate",
+ "AE-UQ": "Imarat Umm al Qaywayn",
+ "AE-RK": "Imarat Ra's al Khaymah",
+ "AE-SH": "Sharjah",
+ "AE-DU": "Dubai",
+ "AE-AZ": "Abu Dhabi",
+ "AE-FU": "Fujairah",
+ "AE-AJ": "Ajman",
+ "IL-D": "Southern District",
+ "IL-HA": "Haifa",
+ "IL-TA": "Tel Aviv",
+ "TR-56": "Siirt",
+ "TR-01": "Adana",
+ "TR-35": "İzmir Province",
+ "TR-30": "Hakkâri",
+ "TR-07": "Antalya",
+ "TR-66": "Yozgat",
+ "TR-63": "Şanlıurfa",
+ "TR-31": "Hatay",
+ "TR-44": "Malatya",
+ "TR-06": "Ankara",
+ "TR-68": "Aksaray",
+ "TR-09": "Aydın",
+ "TR-42": "Konya",
+ "TR-58": "Sivas",
+ "TR-21": "Diyarbakır Province",
+ "TR-65": "Van",
+ "TR-64": "Uşak",
+ "TR-51": "Niğde Province",
+ "TR-73": "Şırnak",
+ "TR-48": "Muğla",
+ "TR-04": "Ağrı",
+ "TR-45": "Manisa",
+ "TR-62": "Tunceli",
+ "TR-12": "Bingöl",
+ "TR-13": "Bitlis",
+ "TR-33": "Mersin",
+ "TR-24": "Erzincan",
+ "TR-38": "Kayseri",
+ "TR-10": "Balıkesir",
+ "TR-23": "Elazığ",
+ "TR-03": "Afyonkarahisar Province",
+ "TR-43": "Kütahya",
+ "TR-26": "Eskişehir",
+ "TR-47": "Mardin",
+ "TR-46": "Kahramanmaraş",
+ "TR-20": "Denizli",
+ "TR-72": "Batman",
+ "TR-02": "Adıyaman Province",
+ "TR-32": "Isparta",
+ "TR-80": "Osmaniye",
+ "TR-79": "Kilis",
+ "TR-27": "Gaziantep",
+ "TR-50": "Nevşehir Province",
+ "TR-49": "Muş",
+ "TR-40": "Kırşehir",
+ "TR-71": "Kırıkkale",
+ "TR-15": "Burdur",
+ "TR-70": "Karaman",
+ "TR-76": "Iğdır",
+ "TR-25": "Erzurum",
+ "TR-11": "Bilecik",
+ "ET-SO": "Somali",
+ "ET-OR": "Oromiya",
+ "ER-SK": "Northern Red Sea",
+ "ER-AN": "Anseba Region",
+ "ET-AM": "Amhara",
+ "ET-HA": "Harari Region",
+ "ET-GA": "Gambela",
+ "ER-DK": "Southern Red Sea Region",
+ "ET-AF": "Afar Region",
+ "ET-DD": "Dire Dawa",
+ "ER-GB": "Gash-Barka Region",
+ "ET-SN": "Southern Nations, Nationalities, and People's Region",
+ "ET-BE": "Bīnshangul Gumuz",
+ "ER-MA": "Maekel Region",
+ "ET-TI": "Tigray",
+ "ER-DU": "Debub Region",
+ "ET-AA": "Addis Ababa",
+ "EG-GH": "Gharbia",
+ "EG-WAD": "New Valley",
+ "EG-KB": "Qalyubia",
+ "EG-DK": "Dakahlia",
+ "EG-MNF": "Monufia",
+ "EG-BNS": "Beni Suweif",
+ "EG-SHG": "Sohag",
+ "EG-MT": "Matruh",
+ "EG-C": "Cairo Governorate",
+ "EG-JS": "South Sinai",
+ "EG-BH": "Beheira",
+ "EG-SIN": "North Sinai",
+ "EG-KN": "Qena",
+ "EG-KFS": "Kafr el-Sheikh",
+ "EG-SHR": "Sharqia",
+ "EG-MN": "Minya",
+ "EG-AST": "Asyut",
+ "EG-GZ": "Giza",
+ "EG-FYM": "Faiyum",
+ "EG-DT": "Damietta Governorate",
+ "EG-ASN": "Aswan",
+ "EG-PTS": "Port Said",
+ "EG-SUZ": "Suez",
+ "EG-ALX": "Alexandria",
+ "EG-LX": "Luxor",
+ "EG-IS": "Ismailia Governorate",
+ "EG-BA": "Red Sea",
+ "AL-12": "Vlorë County",
+ "SD-NO": "Northern",
+ "SD-GZ": "Al Jazīrah",
+ "SD-DN": "Northern Darfur",
+ "SD-KH": "Khartoum",
+ "SD-SI": "Sinnār",
+ "SD-NW": "White Nile",
+ "SD-DS": "Southern Darfur",
+ "SD-KA": "Kassala",
+ "SD-KS": "Southern Kordofan",
+ "SS-EC": "Central Equatoria",
+ "SD-RS": "Red Sea",
+ "SD-DC": "Central Darfur",
+ "SD-DE": "Eastern Darfur",
+ "SD-NB": "Blue Nile",
+ "SD-KN": "North Kordofan",
+ "SD-GK": "West Kordofan State",
+ "SD-GD": "Al Qaḑārif",
+ "SD-DW": "Western Darfur",
+ "SD-NR": "River Nile",
+ "SO-SO": "Sool",
+ "YE-AD": "Aden",
+ "BI-MA": "Makamba Province",
+ "BI-BR": "Bururi Province",
+ "BI-RM": "Rumonge",
+ "BI-MW": "Mwaro",
+ "BI-BL": "Bujumbura Rural Province",
+ "BI-BM": "Bujumbura Mairie Province",
+ "BI-MU": "Muramvya Province",
+ "BI-GI": "Gitega Province",
+ "BI-RY": "Ruyigi Province",
+ "BI-CA": "Cankuzo Province",
+ "BI-BB": "Bubanza Province",
+ "BI-CI": "Cibitoke Province",
+ "BI-NG": "Ngozi Province",
+ "BI-KY": "Kayanza Province",
+ "BI-MY": "Muyinga Province",
+ "BI-KI": "Kirundo Province",
+ "BI-RT": "Rutana Province",
+ "SC-04": "Au Cap",
+ "RU-TVE": "Tver Oblast",
+ "RU-ORE": "Orenburg Oblast",
+ "LV-113": "Valmiera",
+ "LV-089": "Saulkrasti Municipality",
+ "LV-058": "Ludza Municipality",
+ "LV-041": "Jelgava Municipality",
+ "LV-080": "Ropaži Municipality",
+ "LV-015": "Balvi Municipality",
+ "LV-042": "Jēkabpils Municipality",
+ "LV-054": "Limbaži Municipality",
+ "LV-016": "Bauska Municipality",
+ "LV-VEN": "Ventspils",
+ "LV-102": "Varakļāni Municipality",
+ "LV-091": "Sigulda Municipality",
+ "LV-101": "Valka",
+ "LV-097": "Talsi Municipality",
+ "LV-112": "South Kurzeme Municipality",
+ "LV-099": "Tukums Municipality",
+ "LV-067": "Ogre",
+ "LV-022": "Cēsis Municipality",
+ "LV-094": "Smiltene Municipality",
+ "LV-050": "Kuldīga Municipality",
+ "LV-002": "Aizkraukle Municipality",
+ "LV-087": "Salaspils Municipality",
+ "LV-088": "Saldus Municipality",
+ "LV-RIX": "Riga",
+ "LV-REZ": "Rezekne",
+ "LV-052": "Ķekava",
+ "LV-073": "Preili Municipality",
+ "LV-062": "Mārupe",
+ "LV-106": "Ventspils Municipality",
+ "LV-059": "Madona Municipality",
+ "LV-056": "Līvāni",
+ "LV-LPX": "Liepaja",
+ "LV-111": "Augsdaugava Municipality",
+ "LV-026": "Dobele Municipality",
+ "LV-047": "Krāslava Municipality",
+ "LV-011": "Ādaži",
+ "LV-JUR": "Jurmala",
+ "LV-JEL": "Jelgava",
+ "LV-068": "Olaine",
+ "LV-033": "Gulbene Municipality",
+ "LV-DGV": "Daugavpils",
+ "LV-077": "Rēzekne Municipality",
+ "LV-007": "Alūksne Municipality",
+ "RU-VLA": "Vladimir Oblast",
+ "RU-MOS": "Moscow Oblast",
+ "RU-CHE": "Chelyabinsk Oblast",
+ "RU-ROS": "Rostov Oblast",
+ "RU-VGG": "Volgograd Oblast",
+ "RU-KLU": "Kaluga Oblast",
+ "RU-SAM": "Samara Oblast",
+ "RU-KRS": "Kursk Oblast",
+ "RU-STA": "Stavropol Kray",
+ "RU-KGD": "Kaliningrad Oblast",
+ "RU-MOW": "Moscow",
+ "RU-SPE": "St.-Petersburg",
+ "RU-TA": "Tatarstan Republic",
+ "RU-TAM": "Tambov Oblast",
+ "RU-NIZ": "Nizhny Novgorod Oblast",
+ "RU-KDA": "Krasnodar Krai",
+ "RU-LIP": "Lipetsk Oblast",
+ "RU-KR": "Karelia",
+ "RU-KO": "Komi",
+ "RU-PER": "Perm Krai",
+ "RU-ME": "Mariy-El Republic",
+ "RU-TUL": "Tula Oblast",
+ "RU-SMO": "Smolensk Oblast",
+ "RU-KIR": "Kirov Oblast",
+ "RU-YAR": "Yaroslavl Oblast",
+ "RU-CU": "Chuvashia",
+ "RU-BA": "Bashkortostan Republic",
+ "RU-LEN": "Leningrad Oblast",
+ "RU-AD": "Adygeya Republic",
+ "RU-VLG": "Vologda Oblast",
+ "RU-UD": "Udmurtiya Republic",
+ "RU-VOR": "Voronezh Oblast",
+ "RU-SAR": "Saratov Oblast",
+ "RU-BEL": "Belgorod Oblast",
+ "RU-KOS": "Kostroma Oblast",
+ "RU-SE": "North Ossetia–Alania",
+ "RU-ORL": "Oryol oblast",
+ "RU-ARK": "Arkhangelskaya",
+ "RU-PSK": "Pskov Oblast",
+ "RU-RYA": "Ryazan Oblast",
+ "RU-BRY": "Bryansk Oblast",
+ "RU-ULY": "Ulyanovsk Oblast",
+ "RU-KB": "Kabardino-Balkariya Republic",
+ "RU-IVA": "Ivanovo Oblast",
+ "RU-SVE": "Sverdlovsk Oblast",
+ "RU-MUR": "Murmansk",
+ "RU-PNZ": "Penza Oblast",
+ "RU-MO": "Mordoviya Republic",
+ "RU-CE": "Chechnya",
+ "RU-NGR": "Novgorod Oblast",
+ "RU-DA": "Dagestan",
+ "RU-AST": "Astrakhan Oblast",
+ "RU-IN": "Ingushetiya Republic",
+ "RU-NEN": "Nenets",
+ "RU-KL": "Kalmykiya Republic",
+ "RU-KC": "Karachayevo-Cherkesiya Republic",
+ "AZ-ZAQ": "Zaqatala Rayon",
+ "AZ-YE": "Yevlax City",
+ "AZ-UCA": "Ujar Rayon",
+ "AZ-SM": "Sumqayit City",
+ "AZ-SKR": "Shamkir Rayon",
+ "AZ-QUS": "Qusar Rayon",
+ "AZ-QAX": "Qakh Rayon",
+ "AZ-QAB": "Qabala Rayon",
+ "AZ-MI": "Mingacevir City",
+ "AZ-QOB": "Gobustan Rayon",
+ "AZ-SAB": "Sabirabad Rayon",
+ "AZ-ABS": "Absheron Rayon",
+ "AZ-BA": "Baku City",
+ "AZ-XAC": "Khachmaz Rayon",
+ "AZ-ISM": "Ismayilli Rayon",
+ "AZ-AGC": "Aghjabadi Rayon",
+ "AZ-GA": "Ganja City",
+ "AZ-SMI": "Shamakhi Rayon",
+ "AZ-BAR": "Barda",
+ "EE-68": "Pärnumaa",
+ "EE-87": "Võrumaa",
+ "EE-45": "Ida-Virumaa",
+ "EE-84": "Viljandimaa",
+ "EE-37": "Harjumaa",
+ "EE-60": "Lääne-Virumaa",
+ "EE-81": "Valgamaa",
+ "EE-79": "Tartu",
+ "EE-52": "Järvamaa",
+ "EE-56": "Lääne",
+ "EE-50": "Jõgevamaa",
+ "EE-71": "Raplamaa",
+ "EE-64": "Põlvamaa",
+ "EE-74": "Saare",
+ "EE-39": "Hiiumaa",
+ "LT-VL": "Vilnius",
+ "LT-KU": "Kaunas",
+ "LT-UT": "Utena",
+ "LT-SA": "Siauliai",
+ "LT-KL": "Klaipėda County",
+ "LT-MR": "Marijampolė County",
+ "LT-TE": "Telsiai",
+ "LT-AL": "Alytus",
+ "LT-PN": "Panevėžys",
+ "LT-TA": "Tauragė County",
+ "UZ-QR": "Karakalpakstan",
+ "SE-BD": "Norrbotten County",
+ "SE-AC": "Västerbotten County",
+ "KZ-47": "Mangistauskaya Oblast'",
+ "KZ-23": "Atyrau Oblysy",
+ "KZ-27": "West Kazakhstan",
+ "KZ-15": "Aktyubinskaya Oblast'",
+ "KZ-35": "Karaganda",
+ "GE-SZ": "Samegrelo and Zemo Svaneti",
+ "GE-MM": "Mtskheta-Mtianeti",
+ "GE-SK": "Shida Kartli",
+ "GE-KA": "Kakheti",
+ "GE-TB": "K'alak'i T'bilisi",
+ "GE-AB": "Abkhazia",
+ "GE-IM": "Imereti",
+ "GE-KK": "Kvemo Kartli",
+ "GE-GU": "Guria",
+ "GE-RL": "Racha-Lechkhumi and Kvemo Svaneti",
+ "UA-59": "Sumy",
+ "GE-AJ": "Achara",
+ "GE-SJ": "Samtskhe-Javakheti",
+ "AM-ER": "Yerevan",
+ "AM-AV": "Armavir",
+ "AM-KT": "Kotayk",
+ "AM-GR": "Gegharkunik",
+ "AM-TV": "Tavush",
+ "AM-LO": "Lori",
+ "AM-SH": "Shirak",
+ "UA-14": "Donetsk",
+ "AM-AG": "Aragatsotn",
+ "MD-ED": "Raionul Edineţ",
+ "MD-GA": "Gagauzia",
+ "MD-NI": "Nisporeni",
+ "MD-AN": "Anenii Noi",
+ "MD-RI": "Rîşcani",
+ "MD-CU": "Chișinău Municipality",
+ "MD-UN": "Ungheni",
+ "MD-TA": "Taraclia",
+ "MD-SN": "Transnistria",
+ "MD-TE": "Teleneşti",
+ "MD-CA": "Cahul",
+ "MD-CT": "Cantemir",
+ "MD-SV": "Raionul Stefan Voda",
+ "MD-BR": "Briceni",
+ "MD-OR": "Orhei",
+ "MD-GL": "Glodeni",
+ "MD-ST": "Strășeni",
+ "MD-SO": "Raionul Soroca",
+ "MD-SD": "Şoldăneşti",
+ "MD-HI": "Hînceşti",
+ "MD-CS": "Raionul Causeni",
+ "MD-RE": "Rezina",
+ "MD-FA": "Făleşti",
+ "MD-SI": "Sîngerei",
+ "MD-DU": "Raionul Dubasari",
+ "MD-OC": "Raionul Ocniţa",
+ "MD-CR": "Criuleni",
+ "MD-FL": "Floreşti",
+ "MD-LE": "Leova",
+ "MD-IA": "Ialoveni",
+ "MD-DR": "Drochia",
+ "MD-CL": "Raionul Calarasi",
+ "MD-CM": "Cimişlia",
+ "MD-BD": "Bender Municipality",
+ "MD-BS": "Basarabeasca",
+ "MD-BA": "Municipiul Balti",
+ "BY-MI": "Minsk",
+ "BY-HO": "Homyel’ Voblasc’",
+ "UA-46": "Lviv",
+ "BY-BR": "Brest",
+ "BY-VI": "Vitebsk",
+ "BY-HR": "Grodnenskaya",
+ "BY-MA": "Mogilev",
+ "BY-HM": "Minsk City",
+ "FI-12": "Ostrobothnia",
+ "FI-11": "Pirkanmaa",
+ "FI-14": "North Ostrobothnia",
+ "FI-17": "Satakunta",
+ "FI-19": "Southwest Finland",
+ "FI-13": "North Karelia",
+ "FI-05": "Kainuu",
+ "FI-09": "Kymenlaakso",
+ "FI-16": "Paijat-Hame Region",
+ "FI-08": "Central Finland",
+ "FI-18": "Uusimaa",
+ "FI-15": "North Savo",
+ "FI-02": "South Karelia Region",
+ "FI-03": "South Ostrobothnia",
+ "FI-06": "Kanta-Häme",
+ "FI-10": "Lapland",
+ "FI-04": "Southern Savonia",
+ "FI-07": "Central Ostrobothnia",
+ "RO-CS": "Caras-Severin",
+ "RO-AB": "Alba",
+ "RO-TR": "Teleorman",
+ "RO-BZ": "Buzau",
+ "RO-HD": "Hunedoara",
+ "RO-BV": "Brasov",
+ "RO-NT": "Neamt",
+ "RO-BR": "Braila",
+ "RO-SJ": "Salaj",
+ "RO-SB": "Sibiu",
+ "RO-AG": "Arges",
+ "RO-MS": "Mures",
+ "RO-MH": "Mehedinti",
+ "RO-BT": "Botosani",
+ "RO-IF": "Ilfov",
+ "RO-AR": "Arad",
+ "RO-SV": "Suceava",
+ "RO-IS": "Iasi",
+ "RO-HR": "Harghita",
+ "RO-DB": "Dambovita",
+ "RO-MM": "Maramureş",
+ "RO-DJ": "Dolj",
+ "RO-GL": "Galati",
+ "RO-VN": "Vrancea",
+ "RO-VS": "Vaslui",
+ "RO-CL": "Calarasi",
+ "RO-TM": "Timis",
+ "RO-CT": "Constanta",
+ "RO-PH": "Prahova",
+ "RO-BN": "Bistrita-Nasaud",
+ "RO-BH": "Bihor",
+ "RO-BC": "Bacau",
+ "RO-GR": "Giurgiu",
+ "RO-IL": "Ialomita",
+ "RO-CV": "Covasna",
+ "RO-CJ": "Cluj",
+ "RO-TL": "Tulcea",
+ "RO-GJ": "Gorj",
+ "RO-OT": "Olt",
+ "RO-SM": "Satu Mare",
+ "RO-VL": "Valcea",
+ "RO-B": "Bucuresti",
+ "UA-71": "Cherkasy Oblast",
+ "UA-09": "Luhansk",
+ "UA-63": "Kharkiv",
+ "UA-35": "Kirovohrad Oblast",
+ "UA-18": "Zhytomyr",
+ "UA-05": "Vinnytsia",
+ "UA-68": "Khmelnytskyi Oblast",
+ "UA-12": "Dnipropetrovsk Oblast",
+ "UA-32": "Kyiv Oblast",
+ "UA-53": "Poltava Oblast",
+ "UA-56": "Rivne",
+ "UA-61": "Ternopil Oblast",
+ "UA-21": "Zakarpattia Oblast",
+ "UA-07": "Volyn",
+ "UA-77": "Chernivtsi",
+ "UA-23": "Zaporizhzhia",
+ "UA-26": "Ivano-Frankivsk Oblast",
+ "UA-51": "Odessa",
+ "UA-43": "Crimea",
+ "UA-48": "Mykolaiv",
+ "UA-74": "Chernihiv",
+ "UA-65": "Kherson Oblast",
+ "UA-40": "Sebastopol City",
+ "UA-30": "Kyiv City",
+ "HU-HB": "Hajdú-Bihar",
+ "HU-BE": "Bekes County",
+ "HU-SZ": "Szabolcs-Szatmár-Bereg",
+ "HU-JN": "Jász-Nagykun-Szolnok",
+ "HU-HE": "Heves megye",
+ "HU-CS": "Csongrad megye",
+ "HU-BZ": "Borsod-Abaúj-Zemplén",
+ "HU-BK": "Bács-Kiskun",
+ "HU-PE": "Pest megye",
+ "SK-PV": "Presov",
+ "SK-KI": "Kosice",
+ "SK-BC": "Banska Bystrica",
+ "BG-03": "Varna",
+ "BG-08": "Dobrich",
+ "BG-09": "Kardzhali",
+ "BG-21": "Smolyan",
+ "BG-23": "Sofia",
+ "BG-16": "Plovdiv",
+ "BG-04": "Veliko Tarnovo",
+ "BG-01": "Blagoevgrad",
+ "BG-10": "Kyustendil",
+ "BG-06": "Vratsa",
+ "BG-02": "Burgas",
+ "BG-19": "Silistra",
+ "BG-17": "Razgrad",
+ "BG-13": "Pazardzhik",
+ "BG-15": "Pleven",
+ "BG-28": "Yambol",
+ "BG-20": "Sliven",
+ "BG-11": "Lovech",
+ "BG-12": "Montana",
+ "BG-22": "Sofia-Capital",
+ "BG-05": "Vidin",
+ "BG-18": "Ruse",
+ "BG-26": "Haskovo",
+ "BG-24": "Stara Zagora",
+ "BG-25": "Targovishte",
+ "BG-07": "Gabrovo",
+ "BG-14": "Pernik",
+ "BG-27": "Shumen",
+ "GR-A": "East Macedonia and Thrace",
+ "GR-69": "Mount Athos",
+ "TR-67": "Zonguldak",
+ "TR-41": "Kocaeli",
+ "TR-34": "Istanbul",
+ "TR-08": "Artvin",
+ "TR-61": "Trabzon",
+ "TR-28": "Giresun",
+ "TR-16": "Bursa Province",
+ "TR-78": "Karabük Province",
+ "TR-29": "Gümüşhane Province",
+ "TR-77": "Yalova",
+ "TR-39": "Kırklareli",
+ "TR-22": "Edirne",
+ "TR-52": "Ordu",
+ "TR-74": "Bartın",
+ "TR-57": "Sinop",
+ "TR-37": "Kastamonu",
+ "TR-36": "Kars Province",
+ "TR-60": "Tokat",
+ "TR-55": "Samsun",
+ "TR-59": "Tekirdağ",
+ "TR-19": "Çorum",
+ "TR-05": "Amasya",
+ "TR-54": "Sakarya",
+ "TR-14": "Bolu",
+ "TR-53": "Rize Province",
+ "TR-18": "Çankırı",
+ "TR-17": "Canakkale",
+ "TR-81": "Duezce",
+ "TR-69": "Bayburt Province",
+ "TR-75": "Ardahan",
+ "PL-06": "Lublin",
+ "PL-14": "Mazovia",
+ "PL-18": "Subcarpathia",
+ "PL-26": "Świętokrzyskie",
+ "PL-12": "Lesser Poland",
+ "PL-28": "Warmia-Masuria",
+ "PL-20": "Podlasie",
+ "PL-10": "Łódź Voivodeship",
+ "NO-54": "Troms og Finnmark",
+ "AL-05": "Gjirokastër County",
+ "AL-06": "Korçë County",
+ "AL-09": "Dibër County",
+ "AL-03": "Elbasan County",
+ "AL-07": "Kukës County",
+ "MK-204": "Zrnovci",
+ "RS-VO": "Vojvodina",
+ "RS-00": "Belgrade",
+ "MK-605": "Zhelino",
+ "RS-15": "Zajecar",
+ "RS-18": "Raska",
+ "RS-24": "Pcinja",
+ "RS-23": "Jablanica",
+ "MK-202": "Vinica",
+ "MK-301": "Vevchani",
+ "RS-11": "Branicevo",
+ "RS-10": "Podunavlje",
+ "MK-404": "Vasilevo",
+ "MK-403": "Valandovo",
+ "RS-09": "Kolubara",
+ "RS-19": "Rasina",
+ "RS-17": "Morava",
+ "RS-12": "Sumadija",
+ "MK-101": "Veles",
+ "MK-609": "Tetovo",
+ "MK-608": "Tearce",
+ "RS-20": "Nisava",
+ "RS-13": "Pomoravlje",
+ "MK-108": "Sveti Nikole",
+ "MK-410": "Strumica",
+ "MK-312": "Struga",
+ "MK-211": "Shtip",
+ "MK-810": "Petrovec",
+ "MK-304": "Debarca",
+ "ME-17": "Rožaje Municipality",
+ "MK-607": "Mavrovo and Rostuša",
+ "MK-107": "Rosoman",
+ "MK-509": "Resen",
+ "MK-705": "Rankovce",
+ "MK-409": "Radovish",
+ "RS-21": "Toplica",
+ "MK-209": "Probishtip",
+ "MK-508": "Prilep",
+ "RS-16": "Zlatibor",
+ "RS-14": "Bor",
+ "MK-311": "Plasnica",
+ "RS-22": "Pirot",
+ "MK-310": "Ohrid",
+ "MK-408": "Novo Selo",
+ "MK-208": "Pehchevo",
+ "MK-106": "Negotino",
+ "MK-502": "Demir Hisar",
+ "MK-506": "Mogila",
+ "MK-603": "Vrapchishte",
+ "MK-814": "Opstina Centar",
+ "MK-703": "Kumanovo",
+ "MK-702": "Kriva Palanka",
+ "MK-701": "Kratovo",
+ "MK-811": "Saraj",
+ "MK-206": "Kochani",
+ "MK-307": "Kichevo",
+ "MK-104": "Kavadarci",
+ "MK-205": "Karbinci",
+ "MK-207": "Makedonska Kamenica",
+ "MK-606": "Jegunovce",
+ "MK-102": "Gradsko",
+ "MK-604": "Gostivar",
+ "MK-808": "Karposh",
+ "MK-313": "Centar Zhupa",
+ "RS-08": "Macva",
+ "MK-405": "Gevgelija",
+ "MK-407": "Konche",
+ "MK-105": "Lozovo",
+ "MK-805": "Gjorche Petrov",
+ "MK-503": "Dolneni",
+ "MK-103": "Demir Kapija",
+ "MK-806": "Zelenikovo",
+ "MK-303": "Debar",
+ "MK-308": "Makedonski Brod",
+ "MK-601": "Bogovinje",
+ "MK-401": "Bogdanci",
+ "MK-501": "Bitola",
+ "MK-201": "Berovo",
+ "MK-813": "Studenichani",
+ "MK-803": "Butel",
+ "MK-816": "Chucher Sandevo",
+ "MK-809": "Kisela Voda",
+ "MK-802": "Arachinovo",
+ "AO-MOX": "Moxico",
+ "NA-CA": "Zambezi Region",
+ "TZ-21": "Ruvuma",
+ "TZ-17": "Mtwara",
+ "TZ-12": "Lindi",
+ "ZW-MN": "Matabeleland North",
+ "ZW-MA": "Manicaland",
+ "ZW-ME": "Mashonaland East Province",
+ "ZW-MV": "Masvingo Province",
+ "ZW-MI": "Midlands Province",
+ "ZW-MW": "Mashonaland West",
+ "ZW-HA": "Harare",
+ "ZW-MS": "Matabeleland South Province",
+ "ZW-BU": "Bulawayo",
+ "ZM-01": "Western Province",
+ "ZM-06": "North-Western Province",
+ "ZM-05": "Northern Province",
+ "ZM-08": "Copperbelt",
+ "ZM-10": "Muchinga",
+ "ZM-07": "Southern Province",
+ "ZM-09": "Lusaka Province",
+ "ZM-03": "Eastern Province",
+ "ZM-02": "Central Province",
+ "KM-M": "Mohéli",
+ "KM-A": "Ndzuwani",
+ "KM-G": "Grande Comore",
+ "CD-HK": "Haut-Katanga",
+ "MW-C": "Central Region",
+ "MW-N": "Northern Region",
+ "MW-S": "Southern Region",
+ "LS-D": "Berea",
+ "LS-G": "Quthing",
+ "LS-H": "Qacha's Nek",
+ "LS-C": "Leribe",
+ "LS-J": "Mokhotlong",
+ "LS-F": "Mohale's Hoek District",
+ "LS-A": "Maseru",
+ "LS-E": "Mafeteng District",
+ "LS-B": "Butha-Buthe",
+ "BW-KG": "Kgalagadi District",
+ "BW-SE": "South-East",
+ "BW-KW": "Kweneng District",
+ "BW-CE": "Central District",
+ "BW-SP": "Selibe Phikwe",
+ "BW-KL": "Kgatleng District",
+ "BW-NW": "North-West",
+ "BW-LO": "Lobatse",
+ "BW-CH": "Chobe District",
+ "BW-SO": "Ngwaketsi",
+ "BW-GA": "Gaborone",
+ "BW-FR": "City of Francistown",
+ "BW-GH": "Ghanzi District",
+ "MU-GP": "Grand Port District",
+ "MU-MO": "Moka District",
+ "MU-PW": "Plaines Wilhems District",
+ "MU-FL": "Flacq District",
+ "MU-PA": "Pamplemousses District",
+ "MU-RR": "Riviere du Rempart District",
+ "MU-BL": "Black River District",
+ "MU-SA": "Savanne District",
+ "MU-AG": "Agalega Islands",
+ "MU-PL": "Port Louis District",
+ "SZ-LU": "Lubombo District",
+ "SZ-SH": "Shiselweni District",
+ "SZ-HH": "Hhohho",
+ "SZ-MA": "Manzini",
+ "ZA-GP": "Gauteng",
+ "ZA-NW": "North West",
+ "ZA-LP": "Limpopo",
+ "ZA-FS": "Orange Free State",
+ "ZA-MP": "Mpumalanga",
+ "ZA-KZN": "KwaZulu-Natal",
+ "ZA-EC": "Eastern Cape",
+ "ZA-NC": "Northern Cape",
+ "ZA-WC": "Western Cape",
+ "MZ-G": "Gaza Province",
+ "MZ-I": "Inhambane Province",
+ "MZ-Q": "Provincia de Zambezia",
+ "MZ-B": "Manica Province",
+ "MZ-T": "Tete",
+ "MZ-P": "Cabo Delgado Province",
+ "MZ-N": "Nampula",
+ "MZ-L": "Maputo Province",
+ "MZ-MPM": "Cidade de Maputo",
+ "MZ-S": "Sofala Province",
+ "LS-K": "Thaba-Tseka",
+ "IR-11": "Sistan and Baluchestan",
+ "TH-77": "Prachuap Khiri Khan",
+ "AF-HEL": "Helmand",
+ "AF-BAL": "Balkh",
+ "AF-KAN": "Kandahar",
+ "AF-KAB": "Kabul",
+ "AF-NAN": "Nangarhar",
+ "AF-HER": "Herat",
+ "AF-GHA": "Ghazni",
+ "AF-PAR": "Parwan",
+ "AF-BGL": "Baghlan",
+ "TH-76": "Phetchaburi",
+ "TH-84": "Surat Thani",
+ "TH-57": "Chiang Rai",
+ "TH-92": "Trang",
+ "TH-80": "Nakhon Si Thammarat",
+ "TH-52": "Lampang",
+ "TH-86": "Chumphon",
+ "TH-83": "Phuket",
+ "TH-82": "Phang Nga",
+ "TH-63": "Tak",
+ "TH-64": "Sukhothai",
+ "TH-50": "Chiang Mai",
+ "TH-70": "Ratchaburi",
+ "TH-85": "Ranong",
+ "TH-62": "Kamphaeng Phet",
+ "TH-56": "Phayao",
+ "TH-58": "Mae Hong Son",
+ "TH-61": "Uthai Thani",
+ "TH-51": "Lamphun",
+ "TH-54": "Phrae",
+ "TH-91": "Satun",
+ "TH-81": "Krabi",
+ "TH-71": "Kanchanaburi",
+ "TH-72": "Suphanburi",
+ "TH-60": "Nakhon Sawan",
+ "TH-18": "Chai Nat",
+ "TH-73": "Nakhon Pathom",
+ "TH-75": "Samut Songkhram",
+ "PK-SD": "Sindh",
+ "PK-IS": "Islamabad",
+ "PK-BA": "Balochistan",
+ "PK-PB": "Punjab",
+ "PK-KP": "Khyber Pakhtunkhwa",
+ "PK-JK": "Azad Jammu and Kashmir",
+ "PK-GB": "Gilgit-Baltistan",
+ "BD-E": "Rajshahi Division",
+ "BD-F": "Rangpur Division",
+ "BD-C": "Dhaka Division",
+ "BD-B": "Chittagong",
+ "BD-G": "Sylhet Division",
+ "BD-H": "Mymensingh Division",
+ "BD-D": "Khulna Division",
+ "BD-A": "Barisal Division",
+ "ID-SU": "North Sumatra",
+ "ID-AC": "Aceh",
+ "UZ-SA": "Samarqand Region",
+ "UZ-QA": "Qashqadaryo",
+ "UZ-SU": "Surxondaryo Region",
+ "UZ-BU": "Bukhara",
+ "TM-M": "Mary",
+ "TM-L": "Lebap",
+ "TJ-KT": "Viloyati Khatlon",
+ "TJ-SU": "Viloyati Sughd",
+ "TJ-RA": "Republican Subordination",
+ "TJ-GB": "Gorno-Badakhshan",
+ "TJ-DU": "Dushanbe",
+ "MY-02": "Kedah",
+ "LK-1": "Western Province",
+ "LK-3": "Southern Province",
+ "LK-6": "North Western Province",
+ "LK-8": "Province of Uva",
+ "LK-9": "Sabaragamuwa Province",
+ "LK-4": "Northern Province",
+ "LK-2": "Central Province",
+ "LK-7": "North Central Province",
+ "LK-5": "Eastern Province",
+ "BT-32": "Trongsa Dzongkhag",
+ "BT-15": "Thimphu District",
+ "BT-TY": "Trashi Yangste",
+ "BT-41": "Trashigang District",
+ "BT-22": "Dagana",
+ "BT-34": "Zhemgang District",
+ "BT-31": "Sarpang District",
+ "BT-23": "Punakha Dzongkhag",
+ "BT-11": "Paro",
+ "BT-42": "Mongar",
+ "BT-44": "Lhuntse",
+ "BT-13": "Haa",
+ "BT-GA": "Gasa",
+ "BT-12": "Chukha",
+ "IN-MH": "Maharashtra",
+ "IN-KA": "Karnataka",
+ "IN-TG": "Telangana",
+ "IN-HR": "Haryana",
+ "IN-NL": "Nagaland",
+ "IN-WB": "West Bengal",
+ "IN-TN": "Tamil Nadu",
+ "IN-GJ": "Gujarat",
+ "IN-KL": "Kerala",
+ "IN-AP": "Andhra Pradesh",
+ "IN-MN": "Manipur",
+ "IN-MP": "Madhya Pradesh",
+ "IN-GA": "Goa",
+ "IN-UP": "Uttar Pradesh",
+ "IN-OR": "Odisha",
+ "IN-UT": "Uttarakhand",
+ "IN-CT": "Chhattisgarh",
+ "IN-HP": "Himachal Pradesh",
+ "IN-AS": "Assam",
+ "IN-RJ": "Rajasthan",
+ "IN-TR": "Tripura",
+ "IN-ML": "Meghalaya",
+ "IN-JH": "Jharkhand",
+ "IN-PB": "Punjab",
+ "IN-BR": "Bihar",
+ "IN-JK": "Jammu and Kashmir",
+ "IN-SK": "Sikkim",
+ "IN-DH": "Dadra and Nagar Haveli and Daman and Diu",
+ "IN-AN": "Andaman and Nicobar",
+ "IN-MZ": "Mizoram",
+ "IN-PY": "Union Territory of Puducherry",
+ "IN-DL": "National Capital Territory of Delhi",
+ "IN-LA": "Ladakh",
+ "IN-LD": "Lakshadweep",
+ "IN-AR": "Arunachal Pradesh",
+ "IN-CH": "Chandigarh",
+ "CN-XZ": "Tibet",
+ "CN-GS": "Gansu",
+ "CN-XJ": "Xinjiang Uyghur Autonomous Region",
+ "CN-QH": "Qinghai",
+ "CN-YN": "Yunnan",
+ "MV-01": "Addu Atoll",
+ "MV-26": "Kaafu Atoll",
+ "MV-23": "Haa Dhaalu Atholhu",
+ "NP-P1": "Province 1",
+ "NP-P7": "Sudurpashchim Pradesh",
+ "NP-P5": "Lumbini Province",
+ "NP-P4": "Gandaki Pradesh",
+ "NP-P6": "Karnali Pradesh",
+ "NP-P3": "Bagmati Province",
+ "NP-P2": "Province 2",
+ "MM-03": "Magway Region",
+ "MM-02": "Bago Region",
+ "MM-05": "Tanintharyi Region",
+ "MM-17": "Shan State",
+ "MM-06": "Yangon",
+ "MM-04": "Mandalay Region",
+ "MM-11": "Kachin State",
+ "MM-15": "Mon State",
+ "MM-01": "Sagaing Region",
+ "MM-12": "Kayah State",
+ "MM-13": "Kayin State",
+ "MM-14": "Chin State",
+ "MM-07": "Ayeyarwady Region",
+ "BT-33": "Bumthang District",
+ "BT-14": "Samtse District",
+ "BT-24": "Wangdue Phodrang Dzongkhag",
+ "BT-45": "Samdrup Jongkhar",
+ "MV-00": "Southern Ari Atoll",
+ "MV-20": "Baa Atholhu",
+ "MV-17": "Dhaalu Atholhu",
+ "MV-14": "Faafu Atholhu",
+ "MV-29": "Gnyaviyani Atoll",
+ "MV-07": "Haa Alifu Atholhu",
+ "MV-03": "Faadhippolhu Atoll",
+ "MV-12": "Meemu Atholhu",
+ "MV-24": "Shaviyani Atholhu",
+ "MV-08": "Thaa Atholhu",
+ "MV-04": "Vaavu Atholhu",
+ "MV-MLE": "Male",
+ "AF-KHO": "Khowst",
+ "UZ-TK": "Tashkent",
+ "KZ-75": "Almaty",
+ "RU-KGN": "Kurgan Oblast",
+ "RU-TYU": "Tyumen Oblast",
+ "RU-ALT": "Altai Krai",
+ "RU-KYA": "Krasnoyarsk Krai",
+ "RU-KEM": "Kemerovo Oblast",
+ "RU-YAN": "Yamalo-Nenets",
+ "RU-KK": "Khakasiya Republic",
+ "RU-KHM": "Khanty-Mansia",
+ "RU-IRK": "Irkutsk Oblast",
+ "RU-AL": "Altai",
+ "RU-TOM": "Tomsk Oblast",
+ "RU-TY": "Republic of Tyva",
+ "RU-OMS": "Omsk Oblast",
+ "RU-NVS": "Novosibirsk Oblast",
+ "UZ-TO": "Tashkent Region",
+ "UZ-XO": "Xorazm Region",
+ "UZ-SI": "Sirdaryo Region",
+ "UZ-NG": "Namangan",
+ "UZ-NW": "Navoiy Region",
+ "UZ-JI": "Jizzakh Region",
+ "UZ-FA": "Fergana",
+ "UZ-AN": "Andijan Region",
+ "MN-057": "Dzavhan Aymag",
+ "MN-046": "Uvs Province",
+ "MN-065": "Govi-Altai Province",
+ "MN-071": "Bayan-OElgiy Aymag",
+ "MN-043": "Hovd",
+ "KZ-63": "East Kazakhstan",
+ "KZ-62": "Ulytau Region",
+ "KZ-10": "Abai Region",
+ "KZ-31": "Zhambyl Oblysy",
+ "KZ-59": "North Kazakhstan",
+ "KZ-11": "Aqmola Oblysy",
+ "KZ-33": "Jetisu Region",
+ "KZ-61": "South Kazakhstan",
+ "KZ-79": "Shymkent",
+ "KZ-39": "Qostanay Oblysy",
+ "KZ-43": "Qyzylorda Oblysy",
+ "KZ-19": "Almaty Oblysy",
+ "KZ-55": "Pavlodar Region",
+ "KZ-71": "Astana",
+ "KG-Y": "Issyk-Kul",
+ "KG-C": "Chuyskaya Oblast'",
+ "KG-J": "Jalal-Abad oblast",
+ "KG-T": "Talas",
+ "KG-O": "Osh Oblasty",
+ "KG-B": "Batken",
+ "KG-GB": "Gorod Bishkek",
+ "MU-RO": "Rodrigues",
+ "CN-GZ": "Guizhou",
+ "CN-AH": "Anhui",
+ "PW-150": "State of Koror",
+ "PW-002": "State of Aimeliik",
+ "PW-004": "State of Airai",
+ "PW-010": "State of Angaur",
+ "PW-212": "State of Melekeok",
+ "PW-227": "State of Ngeremlengui",
+ "VN-22": "Tinh Nghe An",
+ "VN-52": "Tinh Soc Trang",
+ "VN-51": "Tinh Tra Vinh",
+ "VN-49": "Tinh Vinh Long",
+ "VN-HN": "Hanoi",
+ "VN-18": "Tinh Ninh Binh",
+ "VN-06": "Tinh Yen Bai",
+ "VN-39": "Tinh GJong Nai",
+ "VN-SG": "Ho Chi Minh",
+ "VN-43": "Tinh Ba Ria-Vung Tau",
+ "VN-68": "Tinh Phu Tho",
+ "VN-73": "Hậu Giang",
+ "VN-70": "Tinh Vinh Phuc",
+ "VN-31": "Tinh Binh GJinh",
+ "VN-34": "Tinh Khanh Hoa",
+ "VN-13": "Tinh Quang Ninh",
+ "VN-32": "Tinh Phu Yen",
+ "VN-07": "Tinh Tuyen Quang",
+ "VN-46": "Tiền Giang",
+ "VN-HP": "Haiphong",
+ "VN-57": "Tinh Binh Duong",
+ "VN-27": "Tinh Quang Nam",
+ "VN-21": "Tinh Thanh Hoa",
+ "VN-CT": "Can Tho",
+ "VN-01": "Tinh Lai Chau",
+ "VN-69": "Tinh Thai Nguyen",
+ "VN-20": "Tinh Thai Binh",
+ "VN-37": "Tây Ninh Province",
+ "VN-41": "Long An",
+ "VN-44": "An Giang",
+ "VN-05": "Tinh Son La",
+ "VN-02": "Tinh Lao Cai",
+ "VN-45": "Tinh GJong Thap",
+ "VN-47": "Tinh Kien Giang",
+ "VN-25": "Tinh Quang Tri",
+ "VN-24": "Tinh Quang Binh",
+ "VN-29": "Quảng Ngãi Province",
+ "VN-30": "Gia Lai",
+ "VN-56": "Tinh Bac Ninh",
+ "VN-14": "Tinh Hoa Binh",
+ "VN-63": "Tinh Ha Nam",
+ "VN-26": "Tinh Thua Thien-Hue",
+ "VN-40": "Tinh Binh Thuan",
+ "VN-36": "Tinh Ninh Thuan",
+ "VN-67": "Tinh Nam GJinh",
+ "VN-54": "Tinh Bac Giang",
+ "VN-09": "Tinh Lang Son",
+ "VN-35": "Tinh Lam GJong",
+ "VN-71": "Tinh Dien Bien",
+ "VN-28": "Kon Tum",
+ "VN-66": "Tinh Hung Yen",
+ "VN-23": "Tinh Ha Tinh",
+ "VN-61": "Tinh Hai Duong",
+ "VN-03": "Tinh Ha Giang",
+ "VN-72": "Dak Nong",
+ "VN-58": "Tinh Binh Phuoc",
+ "VN-DN": "Da Nang",
+ "VN-33": "Đắk Lắk",
+ "VN-50": "Tinh Ben Tre",
+ "VN-55": "Tinh Bac Lieu",
+ "VN-04": "Tinh Cao Bang",
+ "VN-59": "Tinh Ca Mau",
+ "VN-53": "Tinh Bac Kan",
+ "TH-16": "Lopburi",
+ "TH-13": "Pathum Thani",
+ "TH-34": "Ubon Ratchathani",
+ "TH-43": "Nong Khai",
+ "TH-41": "Udon Thani",
+ "TH-39": "Nong Bua Lam Phu",
+ "TH-96": "Narathiwat",
+ "TH-35": "Yasothon",
+ "TH-94": "Pattani",
+ "TH-10": "Bangkok",
+ "TH-46": "Kalasin",
+ "TH-95": "Yala",
+ "TH-19": "Saraburi",
+ "TH-67": "Phetchabun",
+ "TH-55": "Nan",
+ "TH-27": "Sa Kaeo",
+ "TH-65": "Phitsanulok",
+ "TH-14": "Phra Nakhon Si Ayutthaya",
+ "TH-33": "Si Sa Ket",
+ "TH-21": "Rayong",
+ "TH-53": "Uttaradit",
+ "TH-23": "Trat",
+ "TH-90": "Songkhla",
+ "TH-45": "Roi Et",
+ "TH-32": "Surin",
+ "TH-48": "Nakhon Phanom",
+ "TH-22": "Chanthaburi",
+ "TH-66": "Phichit",
+ "TH-30": "Nakhon Ratchasima",
+ "TH-20": "Chon Buri",
+ "TH-17": "Sing Buri",
+ "TH-25": "Prachin Buri",
+ "TH-47": "Sakon Nakhon",
+ "TH-31": "Buriram",
+ "TH-24": "Chachoengsao",
+ "TH-74": "Samut Sakhon",
+ "TH-11": "Samut Prakan",
+ "TH-12": "Nonthaburi",
+ "TH-40": "Khon Kaen",
+ "TH-42": "Loei",
+ "TH-44": "Maha Sarakham",
+ "TH-93": "Phatthalung",
+ "TH-37": "Amnat Charoen",
+ "TH-38": "Bueng Kan",
+ "TH-26": "Nakhon Nayok",
+ "TH-49": "Mukdahan",
+ "TH-36": "Chaiyaphum",
+ "TH-15": "Ang Thong",
+ "ID-YO": "Yogyakarta",
+ "ID-JT": "Central Java",
+ "ID-JI": "East Java",
+ "ID-SN": "South Sulawesi",
+ "TL-VI": "Viqueque",
+ "ID-SG": "Southeast Sulawesi",
+ "ID-JB": "West Java",
+ "ID-BA": "Bali",
+ "ID-SA": "North Sulawesi",
+ "ID-PT": "Central Papua",
+ "ID-MU": "North Maluku",
+ "ID-LA": "Lampung",
+ "ID-KI": "East Kalimantan",
+ "ID-RI": "Riau",
+ "ID-JK": "Jakarta",
+ "ID-KU": "North Kalimantan",
+ "ID-KR": "Riau Islands",
+ "ID-KS": "South Kalimantan",
+ "ID-BT": "Banten",
+ "ID-JA": "Jambi",
+ "ID-BB": "Bangka–Belitung Islands",
+ "ID-NB": "West Nusa Tenggara",
+ "ID-SS": "South Sumatra",
+ "TL-CO": "Cova Lima",
+ "ID-SB": "West Sumatra",
+ "ID-PS": "South Papua",
+ "ID-KB": "West Kalimantan",
+ "ID-KT": "Central Kalimantan",
+ "TL-MF": "Manufahi",
+ "ID-SR": "West Sulawesi",
+ "ID-MA": "Maluku",
+ "TL-OE": "Oecusse",
+ "ID-ST": "Central Sulawesi",
+ "ID-PB": "West Papua",
+ "TL-MT": "Manatuto",
+ "TL-BO": "Bobonaro",
+ "TL-LI": "Liquiçá",
+ "ID-NT": "East Nusa Tenggara",
+ "ID-PA": "Papua",
+ "TL-AN": "Ainaro",
+ "ID-GO": "Gorontalo",
+ "TL-DI": "Dili",
+ "ID-BE": "Bengkulu",
+ "TL-BA": "Baucau",
+ "TL-AL": "Aileu",
+ "LA-VT": "Vientiane Prefecture",
+ "LA-HO": "Houaphan",
+ "LA-SV": "Khoueng Savannakhet",
+ "LA-SL": "Salavan",
+ "LA-CH": "Champasak",
+ "LA-XA": "Xaignabouli",
+ "LA-VI": "Vientiane Province",
+ "LA-BL": "Bolikhamsai",
+ "LA-KH": "Khammouan",
+ "LA-LM": "Louangnamtha",
+ "LA-XI": "Xiangkhouang",
+ "LA-XS": "Xaisomboun",
+ "LA-PH": "Khoueng Phongsali",
+ "LA-OU": "Khoueng Oudomxai",
+ "LA-AT": "Attapu",
+ "TW-NWT": "New Taipei",
+ "TW-YUN": "Yunlin",
+ "TW-TNN": "Tainan",
+ "TW-CHA": "Changhua",
+ "TW-NAN": "Nantou",
+ "TW-ILA": "Yilan",
+ "TW-KHH": "Kaohsiung",
+ "TW-TAO": "Taoyuan",
+ "TW-TXG": "Taichung City",
+ "TW-HUA": "Hualien",
+ "TW-PIF": "Pingtung",
+ "TW-MIA": "Miaoli",
+ "TW-CYQ": "Chiayi County",
+ "TW-HSZ": "Hsinchu County",
+ "TW-TTT": "Taitung",
+ "TW-TPE": "Taipei City",
+ "TW-PEN": "Penghu County",
+ "TW-LIE": "Lienchiang",
+ "TW-HSQ": "Hsinchu",
+ "TW-KIN": "Kinmen County",
+ "TW-KEE": "Keelung",
+ "TW-CYI": "Chiayi",
+ "PH-06": "Western Visayas",
+ "PH-03": "Central Luzon",
+ "PH-09": "Zamboanga Peninsula",
+ "PH-14": "Autonomous Region in Muslim Mindanao",
+ "PH-05": "Bicol",
+ "PH-01": "Ilocos",
+ "PH-41": "Mimaropa",
+ "PH-40": "Calabarzon",
+ "PH-00": "Metro Manila",
+ "PH-07": "Central Visayas",
+ "PH-10": "Northern Mindanao",
+ "PH-02": "Cagayan Valley",
+ "PH-12": "Soccsksargen",
+ "PH-15": "Cordillera",
+ "PH-08": "Eastern Visayas",
+ "PH-13": "Caraga",
+ "PH-11": "Davao",
+ "MY-07": "Penang",
+ "MY-06": "Pahang",
+ "MY-08": "Perak",
+ "MY-10": "Selangor",
+ "MY-01": "Johor",
+ "MY-11": "Terengganu",
+ "MY-13": "Sarawak",
+ "MY-12": "Sabah",
+ "MY-03": "Kelantan",
+ "MY-14": "Kuala Lumpur",
+ "MY-15": "Labuan",
+ "MY-04": "Melaka",
+ "MY-05": "Negeri Sembilan",
+ "MY-09": "Perlis",
+ "CN-SD": "Shandong",
+ "CN-SC": "Sichuan",
+ "CN-HN": "Hunan",
+ "CN-HA": "Henan",
+ "CN-ZJ": "Zhejiang",
+ "CN-LN": "Liaoning",
+ "CN-NX": "Ningxia Hui Autonomous Region",
+ "CN-JS": "Jiangsu",
+ "CN-HE": "Hebei",
+ "CN-SX": "Shanxi",
+ "CN-GD": "Guangdong",
+ "CN-FJ": "Fujian",
+ "CN-HB": "Hubei",
+ "CN-GX": "Guangxi",
+ "CN-JX": "Jiangxi",
+ "CN-HI": "Hainan",
+ "CN-TJ": "Tianjin",
+ "CN-SN": "Shaanxi",
+ "CN-SH": "Shanghai",
+ "CN-CQ": "Chongqing",
+ "CN-BJ": "Beijing",
+ "HK-NIS": "Islands District",
+ "HK-NYL": "Yuen Long District",
+ "HK-KYT": "Yau Tsim Mong",
+ "HK-KWT": "Wong Tai Sin",
+ "HK-HSO": "Southern",
+ "HK-HWC": "Wan Chai",
+ "HK-HEA": "Eastern",
+ "HK-NTM": "Tuen Mun",
+ "HK-NSK": "Sai Kung District",
+ "HK-NKT": "Kwai Tsing",
+ "HK-NTW": "Tsuen Wan District",
+ "HK-KKC": "Kowloon City",
+ "HK-NTP": "Tai Po District",
+ "HK-NST": "Sha Tin",
+ "HK-NNO": "North",
+ "HK-HCW": "Central and Western District",
+ "HK-KSS": "Sham Shui Po",
+ "HK-KKT": "Kwun Tong",
+ "BN-TU": "Tutong",
+ "BN-BE": "Belait",
+ "BN-BM": "Brunei-Muara District",
+ "BN-TE": "Temburong",
+ "KH-12": "Phnom Penh",
+ "KH-21": "Takeo",
+ "KH-20": "Svay Rieng",
+ "KH-19": "Stung Treng",
+ "KH-17": "Siem Reap",
+ "KH-11": "Mondolkiri",
+ "KH-14": "Prey Veng",
+ "KH-15": "Pursat",
+ "KH-5": "Kampong Speu",
+ "KH-22": "Otar Meanchey",
+ "KH-4": "Kampong Chhnang",
+ "KH-25": "Tboung Khmum",
+ "KH-8": "Kandal",
+ "KH-1": "Banteay Meanchey",
+ "KH-6": "Kampong Thom",
+ "KH-13": "Preah Vihear",
+ "KH-24": "Pailin",
+ "KH-23": "Kep",
+ "KH-9": "Koh Kong",
+ "KH-10": "Kratie",
+ "KH-18": "Preah Sihanouk",
+ "KH-3": "Kampong Cham",
+ "KH-2": "Battambang",
+ "KR-47": "Gyeongsangbuk-do",
+ "KR-30": "Daejeon",
+ "KR-42": "Gangwon-do",
+ "KR-11": "Seoul",
+ "KR-46": "Jeollanam-do",
+ "KR-41": "Gyeonggi-do",
+ "KR-43": "North Chungcheong",
+ "KR-48": "Gyeongsangnam-do",
+ "KR-26": "Busan",
+ "KR-44": "Chungcheongnam-do",
+ "KR-45": "Jeollabuk-do",
+ "KR-29": "Gwangju",
+ "KR-31": "Ulsan",
+ "KR-27": "Daegu",
+ "KR-49": "Jeju-do",
+ "KR-28": "Incheon",
+ "JP-30": "Wakayama",
+ "JP-17": "Ishikawa",
+ "JP-14": "Kanagawa",
+ "JP-28": "Hyōgo",
+ "JP-25": "Shiga",
+ "JP-37": "Kagawa",
+ "JP-22": "Shizuoka",
+ "JP-31": "Tottori",
+ "JP-21": "Gifu",
+ "JP-06": "Yamagata",
+ "JP-35": "Yamaguchi",
+ "JP-40": "Fukuoka",
+ "JP-08": "Ibaraki",
+ "JP-13": "Tokyo",
+ "JP-29": "Nara",
+ "JP-11": "Saitama",
+ "JP-15": "Niigata",
+ "JP-20": "Nagano",
+ "JP-19": "Yamanashi",
+ "JP-47": "Okinawa",
+ "JP-33": "Okayama",
+ "JP-10": "Gunma",
+ "JP-24": "Mie",
+ "JP-44": "Oita",
+ "JP-12": "Chiba",
+ "JP-23": "Aichi",
+ "JP-26": "Kyoto",
+ "JP-43": "Kumamoto",
+ "JP-32": "Shimane",
+ "JP-27": "Ōsaka",
+ "JP-46": "Kagoshima",
+ "JP-09": "Tochigi",
+ "JP-18": "Fukui",
+ "JP-36": "Tokushima",
+ "JP-42": "Nagasaki",
+ "JP-34": "Hiroshima",
+ "JP-39": "Kochi",
+ "JP-16": "Toyama",
+ "JP-45": "Miyazaki",
+ "JP-41": "Saga",
+ "JP-05": "Akita",
+ "JP-07": "Fukushima",
+ "JP-38": "Ehime",
+ "KP-01": "Pyongyang",
+ "TL-LA": "Lautém",
+ "TL-ER": "Ermera",
+ "RU-ZAB": "Transbaikal Territory",
+ "RU-AMU": "Amur Oblast",
+ "RU-BU": "Buryatiya Republic",
+ "RU-SA": "Sakha",
+ "RU-PRI": "Primorye",
+ "RU-KHA": "Khabarovsk",
+ "RU-YEV": "Yevrey (Jewish) Autonomous Oblast",
+ "MN-1": "Ulaanbaatar Hot",
+ "MN-067": "Bulgan",
+ "MN-073": "Arkhangai Province",
+ "MN-059": "Middle Govĭ",
+ "MN-053": "Ömnögovĭ",
+ "MN-051": "Suehbaatar Aymag",
+ "MN-039": "Hentiy Aymag",
+ "MN-063": "East Gobi Aymag",
+ "MN-041": "Khövsgöl Province",
+ "MN-035": "Orhon Aymag",
+ "MN-055": "South Khangay",
+ "MN-047": "Central Aymag",
+ "MN-064": "Govi-Sumber",
+ "MN-061": "East Aimak",
+ "MN-069": "Bayanhongor Aymag",
+ "MN-049": "Selenge Aymag",
+ "CN-HL": "Heilongjiang",
+ "CN-JL": "Jilin",
+ "CN-NM": "Inner Mongolia Autonomous Region",
+ "KP-04": "Chagang-do",
+ "AU-SA": "South Australia",
+ "AU-WA": "Western Australia",
+ "AU-NT": "Northern Territory",
+ "AU-QLD": "Queensland",
+ "FM-TRK": "State of Chuuk",
+ "FM-KSA": "State of Kosrae",
+ "FM-PNI": "State of Pohnpei",
+ "PG-ESW": "East Sepik Province",
+ "PG-EPW": "Enga Province",
+ "PG-SAN": "West Sepik Province",
+ "PG-EBR": "East New Britain Province",
+ "PG-NCD": "National Capital",
+ "PG-NPP": "Northern Province",
+ "PG-NSB": "Bougainville",
+ "PG-GPK": "Gulf Province",
+ "PG-WHM": "Western Highlands Province",
+ "PG-NIK": "New Ireland",
+ "PG-SHM": "Southern Highlands Province",
+ "PG-MPM": "Madang Province",
+ "PG-MRL": "Manus Province",
+ "PG-MPL": "Morobe Province",
+ "PG-CPK": "Chimbu Province",
+ "PG-CPM": "Central Province",
+ "PG-HLA": "Hela",
+ "PG-WBK": "West New Britain Province",
+ "PG-EHG": "Eastern Highlands Province",
+ "PG-WPD": "Western Province",
+ "SB-CE": "Central Province",
+ "SB-WE": "Western Province",
+ "SB-GU": "Guadalcanal Province",
+ "SB-CT": "Honiara",
+ "SB-IS": "Isabel Province",
+ "SB-ML": "Malaita Province",
+ "KI-G": "Gilbert Islands",
+ "TV-NUI": "Nui",
+ "TV-FUN": "Funafuti",
+ "TV-VAI": "Vaitupu",
+ "NR-01": "Aiwo",
+ "NR-13": "Uaboe",
+ "NR-03": "Anetan",
+ "NR-05": "Baiti",
+ "NR-10": "Ijuw",
+ "NR-09": "Ewa",
+ "NR-04": "Anibare",
+ "NR-02": "Anabar",
+ "JP-04": "Miyagi",
+ "JP-03": "Iwate",
+ "MH-MAJ": "Majuro Atoll",
+ "RU-KAM": "Kamchatka",
+ "RU-SAK": "Sakhalin Oblast",
+ "RU-MAG": "Magadan Oblast",
+ "RU-CHU": "Chukotka",
+ "JP-01": "Hokkaido",
+ "JP-02": "Aomori",
+ "PG-MBA": "Milne Bay Province",
+ "VU-TOB": "Torba",
+ "VU-SEE": "Shefa Province",
+ "VU-MAP": "Malampa Province",
+ "VU-SAM": "Sanma Province",
+ "VU-PAM": "Penama Province",
+ "VU-TAE": "Tafea Province",
+ "NC-L": "Loyalty Islands",
+ "NC-N": "North Province",
+ "NC-S": "South Province",
+ "AU-NSW": "New South Wales",
+ "AU-VIC": "Victoria",
+ "AU-TAS": "Tasmania",
+ "AU-ACT": "Australian Capital Territory",
+ "SB-RB": "Rennell and Bellona",
+ "SB-TE": "Temotu Province",
+ "SB-MK": "Makira-Ulawa Province",
+ "NZ-AUK": "Auckland",
+ "NZ-STL": "Southland",
+ "NZ-CAN": "Canterbury",
+ "NZ-MBH": "Marlborough",
+ "NZ-OTA": "Otago",
+ "NZ-WKO": "Waikato",
+ "NZ-GIS": "Gisborne",
+ "NZ-BOP": "Bay of Plenty",
+ "NZ-WGN": "Wellington",
+ "NZ-TKI": "Taranaki",
+ "NZ-MWT": "Manawatu-Wanganui",
+ "NZ-TAS": "Tasman",
+ "NZ-NTL": "Northland",
+ "NZ-HKB": "Hawke's Bay",
+ "NZ-WTC": "West Coast",
+ "NZ-NSN": "Nelson",
+ "FJ-C": "Central",
+ "FJ-W": "Western",
+ "FJ-N": "Northern",
+ "FJ-R": "Rotuma",
+ "LY-NQ": "An Nuqat al Khams",
+ "LY-MI": "Sha'biyat Misratah",
+ "LY-JU": "Al Jufrah",
+ "LY-TB": "Tripoli",
+ "LY-SR": "Surt",
+ "LY-ZA": "Az Zawiyah",
+ "LY-SB": "Sabha",
+ "LY-NL": "Nalut",
+ "LY-MQ": "Murzuq",
+ "LY-JG": "Jabal al Gharbi",
+ "LY-GT": "Ghat",
+ "LY-WD": "Wadi al Hayat",
+ "LY-MB": "Al Marqab",
+ "CM-CE": "Centre",
+ "CM-NW": "North-West Region",
+ "CM-AD": "Adamaoua Region",
+ "CM-LT": "Littoral",
+ "CM-EN": "Far North Region",
+ "CM-SU": "South",
+ "CM-SW": "South-West Region",
+ "CM-OU": "West Region",
+ "AO-LUA": "Luanda Province",
+ "AO-UIG": "Uíge",
+ "AO-ZAI": "Zaire",
+ "AO-MAL": "Malanje Province",
+ "AO-BGO": "Bengo Province",
+ "AO-CNO": "Cuanza Norte Province",
+ "AO-CUS": "Kwanza Sul",
+ "SN-ZG": "Ziguinchor",
+ "SN-TH": "Region de Thies",
+ "SN-SE": "Region de Sedhiou",
+ "SN-FK": "Fatick",
+ "SN-MT": "Matam",
+ "SN-DB": "Diourbel",
+ "SN-SL": "Saint-Louis",
+ "SN-DK": "Dakar",
+ "SN-KE": "Region de Kedougou",
+ "SN-KL": "Kaolack",
+ "SN-LG": "Louga",
+ "SN-KD": "Kolda",
+ "SN-TC": "Tambacounda",
+ "SN-KA": "Region de Kaffrine",
+ "CG-2": "Lékoumou",
+ "CG-16": "Pointe-Noire",
+ "CG-13": "Sangha",
+ "CG-8": "Cuvette",
+ "CG-9": "Niari",
+ "CG-BZV": "Brazzaville",
+ "CG-7": "Likouala",
+ "CG-5": "Kouilou",
+ "CG-15": "Cuvette-Ouest",
+ "CG-14": "Plateaux",
+ "CG-12": "Pool",
+ "PT-14": "Santarém",
+ "PT-02": "Beja",
+ "PT-07": "Évora",
+ "PT-11": "Lisbon",
+ "PT-08": "Faro",
+ "PT-15": "Setúbal",
+ "PT-04": "Bragança",
+ "PT-05": "Castelo Branco",
+ "PT-10": "Leiria",
+ "PT-12": "Portalegre",
+ "PT-30": "Madeira",
+ "LR-GG": "Grand Gedeh County",
+ "LR-GK": "Grand Kru County",
+ "LR-LO": "Lofa County",
+ "LR-BM": "Bomi County",
+ "LR-GP": "Gbarpolu County",
+ "LR-NI": "Nimba County",
+ "LR-RI": "River Cess County",
+ "LR-MO": "Montserrado County",
+ "LR-MG": "Margibi County",
+ "LR-MY": "Maryland County",
+ "LR-SI": "Sinoe County",
+ "LR-BG": "Bong County",
+ "LR-GB": "Grand Bassa County",
+ "CI-YM": "Yamoussoukro Autonomous District",
+ "CI-LC": "Lacs",
+ "CI-AB": "Abidjan",
+ "CI-ZZ": "Zanzan",
+ "CI-BS": "Bas-Sassandra",
+ "CI-GD": "Goh-Djiboua",
+ "CI-DN": "Denguele",
+ "CI-WR": "Woroba",
+ "CI-MG": "Montagnes",
+ "CI-SV": "Savanes",
+ "CI-CM": "Comoe",
+ "CI-SM": "Sassandra-Marahoue",
+ "CI-LG": "Lagunes",
+ "CI-VB": "Vallee du Bandama",
+ "GH-NP": "Northern Region",
+ "GH-UW": "Upper West Region",
+ "GH-AA": "Greater Accra Region",
+ "GH-AH": "Ashanti Region",
+ "GH-WP": "Western Region",
+ "GH-BO": "Bono",
+ "GH-EP": "Eastern Region",
+ "GH-CP": "Central Region",
+ "GH-WN": "Western North",
+ "GH-UE": "Upper East Region",
+ "GH-NE": "North East",
+ "GH-AF": "Ahafo",
+ "GH-BE": "Bono East",
+ "GH-TV": "Volta Region",
+ "GH-OT": "Oti",
+ "GH-SV": "Savannah",
+ "GQ-AN": "Annobon",
+ "GQ-WN": "Wele-Nzas",
+ "GQ-KN": "Kié-Ntem",
+ "GQ-CS": "Centro Sur",
+ "GQ-BN": "Bioko Norte",
+ "GQ-BS": "Bioko Sur",
+ "GQ-LI": "Litoral",
+ "CD-KG": "Kwango",
+ "CD-KN": "Kinshasa City",
+ "CD-KL": "Kwilu",
+ "CD-MN": "Mai-Ndombe",
+ "CD-BC": "Bas-Congo",
+ "NG-KD": "Kaduna State",
+ "NG-AD": "Adamawa",
+ "NG-LA": "Lagos",
+ "NG-TA": "Taraba State",
+ "NG-DE": "Delta",
+ "NG-NA": "Nasarawa State",
+ "NG-AK": "Akwa Ibom State",
+ "NG-AB": "Abia State",
+ "NG-BO": "Borno State",
+ "NG-YO": "Yobe State",
+ "NG-SO": "Sokoto State",
+ "NG-OG": "Ogun State",
+ "NG-RI": "Rivers State",
+ "NG-GO": "Gombe State",
+ "NG-OY": "Oyo State",
+ "NG-IM": "Imo State",
+ "NG-OS": "Osun State",
+ "NG-KW": "Kwara State",
+ "NG-AN": "Anambra",
+ "NG-ON": "Ondo State",
+ "NG-KO": "Kogi State",
+ "NG-EN": "Enugu State",
+ "NG-CR": "Cross River State",
+ "NG-FC": "FCT",
+ "NG-NI": "Niger State",
+ "NG-PL": "Plateau State",
+ "NG-KT": "Katsina State",
+ "NG-KN": "Kano State",
+ "NG-KE": "Kebbi",
+ "NG-EB": "Ebonyi State",
+ "NG-ED": "Edo",
+ "NG-BA": "Bauchi",
+ "NG-ZA": "Zamfara State",
+ "NG-EK": "Ekiti State",
+ "BF-11": "Plateau-Central",
+ "BF-04": "Centre-Est",
+ "BF-10": "Nord",
+ "BF-03": "Centre",
+ "BF-07": "Centre-Sud",
+ "BF-01": "Boucle du Mouhoun",
+ "BF-05": "Centre-Nord",
+ "BF-12": "Sahel",
+ "BF-13": "Sud-Ouest",
+ "BF-08": "Est",
+ "BF-09": "Hauts-Bassins",
+ "BF-02": "Cascades Region",
+ "TG-C": "Centrale",
+ "TG-S": "Savanes",
+ "TG-M": "Maritime",
+ "TG-P": "Plateaux",
+ "TG-K": "Kara",
+ "GW-BM": "Biombo",
+ "GW-GA": "Gabu",
+ "GW-CA": "Cacheu Region",
+ "GW-BA": "Bafata",
+ "GW-BL": "Bolama",
+ "GW-QU": "Quinara",
+ "GW-OI": "Oio Region",
+ "GW-BS": "Bissau",
+ "MR-09": "Tagant",
+ "MR-02": "Hodh El Gharbi",
+ "MR-06": "Trarza",
+ "MR-01": "Hodh Ech Chargi",
+ "MR-04": "Gorgol",
+ "MR-03": "Assaba",
+ "MR-11": "Tiris Zemmour",
+ "MR-10": "Guidimaka",
+ "MR-07": "Adrar",
+ "MR-05": "Brakna",
+ "MR-12": "Inchiri",
+ "CF-OP": "Ouham-Pendé",
+ "CF-SE": "Sangha-Mbaéré",
+ "CF-LB": "Lobaye",
+ "CF-KB": "Nana-Grébizi",
+ "CF-AC": "Ouham",
+ "CF-KG": "Kémo",
+ "CF-MP": "Ombella-M'Poko",
+ "CF-HS": "Mambéré-Kadéï",
+ "CF-NM": "Nana-Mambéré",
+ "CF-BGF": "Bangui",
+ "BJ-CO": "Collines Department",
+ "BJ-AK": "Atakora Department",
+ "BJ-OU": "Ouémé",
+ "BJ-BO": "Borgou Department",
+ "BJ-AQ": "Atlantique Department",
+ "BJ-AL": "Alibori",
+ "BJ-MO": "Mono",
+ "BJ-KO": "Kouffo Department",
+ "BJ-DO": "Donga",
+ "BJ-LI": "Littoral",
+ "BJ-ZO": "Zou Department",
+ "GA-8": "Ogooué-Maritime",
+ "GA-9": "Woleu-Ntem",
+ "GA-3": "Moyen-Ogooué",
+ "GA-5": "Nyanga",
+ "GA-4": "Ngouni",
+ "GA-6": "Ogooué-Ivindo",
+ "GA-1": "Estuaire",
+ "GA-7": "Ogooué-Lolo",
+ "GA-2": "Haut-Ogooué",
+ "SL-W": "Western Area",
+ "SL-NW": "North West",
+ "SL-N": "Northern Province",
+ "SL-E": "Eastern Province",
+ "SL-S": "Southern Province",
+ "ST-01": "Agua Grande",
+ "ST-P": "Principe",
+ "ST-04": "Lemba",
+ "SH-AC": "Ascension",
+ "GM-W": "West Coast",
+ "GM-L": "Lower River Division",
+ "GM-B": "Banjul",
+ "GM-M": "Central River",
+ "GM-N": "North Bank",
+ "GN-M": "Mamou Region",
+ "GN-L": "Labe Region",
+ "GN-C": "Conakry Region",
+ "GN-B": "Boke Region",
+ "GN-F": "Faranah",
+ "GN-D": "Kindia",
+ "GN-K": "Kankan Region",
+ "TD-CB": "Chari-Baguirmi Region",
+ "TD-MO": "Mayo-Kebbi Ouest",
+ "TD-MC": "Moyen-Chari Region",
+ "TD-BG": "Barh el Gazel",
+ "TD-TA": "Tandjilé",
+ "TD-KA": "Kanem Region",
+ "TD-ND": "N’Djaména",
+ "TD-LO": "Logone Occidental Region",
+ "TD-GR": "Guéra",
+ "TD-MA": "Mandoul",
+ "TD-HL": "Hadjer-Lamis",
+ "TD-BO": "Borkou Region",
+ "TD-ME": "Mayo-Kebbi Est",
+ "TD-LC": "Lac Region",
+ "TD-LR": "Logone Oriental Region",
+ "TD-BA": "Batha Region",
+ "TD-TI": "Tibesti Region",
+ "NE-5": "Tahoua",
+ "NE-7": "Zinder",
+ "NE-6": "Tillaberi Region",
+ "NE-4": "Maradi",
+ "NE-8": "Niamey",
+ "NE-3": "Dosso Region",
+ "NE-2": "Diffa",
+ "NE-1": "Agadez",
+ "ML-6": "Tombouctou",
+ "ML-7": "Gao",
+ "ML-10": "Taoudénit",
+ "ML-3": "Sikasso",
+ "ML-4": "Ségou",
+ "ML-5": "Mopti",
+ "ML-2": "Koulikoro",
+ "ML-8": "Kidal",
+ "ML-1": "Kayes",
+ "ML-BKO": "Bamako Region",
+ "MA-11": "Laayoune-Sakia El Hamra",
+ "MA-12": "Dakhla-Oued Ed-Dahab",
+ "TN-22": "Zaghouan Governorate",
+ "TN-32": "Jendouba Governorate",
+ "TN-31": "Béja Governorate",
+ "TN-14": "Manouba",
+ "TN-71": "Gafsa",
+ "TN-11": "Tunis Governorate",
+ "TN-72": "Tozeur Governorate",
+ "TN-83": "Tataouine",
+ "TN-42": "Kasserine Governorate",
+ "TN-21": "Nabeul Governorate",
+ "TN-33": "Kef Governorate",
+ "TN-52": "Monastir Governorate",
+ "TN-51": "Sousse Governorate",
+ "TN-12": "Ariana Governorate",
+ "TN-34": "Siliana Governorate",
+ "TN-53": "Mahdia Governorate",
+ "TN-43": "Sidi Bouzid Governorate",
+ "TN-61": "Sfax Governorate",
+ "TN-13": "Ben Arous Governorate",
+ "TN-73": "Kebili Governorate",
+ "TN-82": "Medenine Governorate",
+ "TN-81": "Gabès Governorate",
+ "TN-23": "Bizerte Governorate",
+ "TN-41": "Kairouan",
+ "DZ-35": "Boumerdes",
+ "DZ-25": "Constantine",
+ "DZ-07": "Biskra",
+ "DZ-16": "Algiers",
+ "DZ-17": "Djelfa",
+ "DZ-33": "Illizi",
+ "DZ-15": "Tizi Ouzou",
+ "DZ-36": "El Tarf",
+ "DZ-55": "Touggourt",
+ "DZ-18": "Jijel",
+ "DZ-13": "Tlemcen",
+ "DZ-38": "Tissemsilt",
+ "DZ-42": "Tipaza",
+ "DZ-37": "Tindouf",
+ "DZ-49": "Timimoun",
+ "DZ-29": "Mascara",
+ "DZ-14": "Tiaret",
+ "DZ-02": "Chlef",
+ "DZ-43": "Mila",
+ "DZ-12": "Tébessa",
+ "DZ-05": "Batna",
+ "DZ-06": "Béjaïa",
+ "DZ-11": "Tamanrasset",
+ "DZ-21": "Skikda",
+ "DZ-26": "Medea",
+ "DZ-10": "Bouira",
+ "DZ-09": "Blida",
+ "DZ-41": "Souk Ahras",
+ "DZ-48": "Relizane",
+ "DZ-31": "Oran",
+ "DZ-46": "Aïn Témouchent",
+ "DZ-22": "Sidi Bel Abbès",
+ "DZ-28": "M'Sila",
+ "DZ-19": "Sétif",
+ "DZ-20": "Saida",
+ "DZ-39": "El Oued",
+ "DZ-01": "Adrar",
+ "DZ-34": "Bordj Bou Arréridj",
+ "DZ-04": "Oum el Bouaghi",
+ "DZ-51": "Ouled Djellal",
+ "DZ-24": "Guelma",
+ "DZ-30": "Ouargla",
+ "DZ-45": "Naama",
+ "DZ-27": "Mostaganem",
+ "DZ-52": "Beni Abbes",
+ "DZ-44": "Aïn Defla",
+ "DZ-03": "Laghouat",
+ "DZ-40": "Khenchela",
+ "DZ-08": "Béchar",
+ "DZ-56": "Djanet",
+ "DZ-53": "In Salah",
+ "DZ-47": "Ghardaia",
+ "DZ-32": "El Bayadh",
+ "DZ-54": "In Guezzam",
+ "DZ-57": "El Mghair",
+ "DZ-23": "Annaba",
+ "DZ-58": "El Menia",
+ "ES-AN": "Andalusia",
+ "ES-EX": "Extremadura",
+ "ES-MC": "Murcia",
+ "ES-CM": "Castille-La Mancha",
+ "ES-VC": "Valencia",
+ "ES-CN": "Canary Islands",
+ "ES-IB": "Balearic Islands",
+ "ES-ML": "Melilla",
+ "IT-78": "Calabria",
+ "IT-82": "Sicily",
+ "IT-88": "Sardinia",
+ "IT-77": "Basilicate",
+ "IT-75": "Apulia",
+ "IT-72": "Campania",
+ "MA-08": "Draa-Tafilalet",
+ "MA-07": "Marrakesh-Safi",
+ "MA-09": "Souss-Massa",
+ "MA-06": "Casablanca-Settat",
+ "MA-05": "Beni Mellal-Khenifra",
+ "MA-04": "Rabat-Sale-Kenitra",
+ "MA-01": "Tanger-Tetouan-Al Hoceima",
+ "MA-03": "Fes-Meknes",
+ "MA-02": "Oriental",
+ "MA-10": "Guelmim-Oued Noun",
+ "MT-28": "Marsaxlokk",
+ "MT-68": "Iz-Zurrieq",
+ "MT-67": "Iz-Zejtun",
+ "MT-65": "Iz-Zebbug",
+ "MT-64": "Haz-Zabbar",
+ "MT-63": "Ix-Xghajra",
+ "MT-62": "Ix-Xewkija",
+ "MT-61": "Ix-Xaghra",
+ "MT-60": "Valletta",
+ "MT-58": "Tarxien",
+ "MT-59": "Ta' Xbiex",
+ "MT-56": "Tas-Sliema",
+ "MT-55": "Is-Siggiewi",
+ "MT-54": "Saint Venera",
+ "MT-53": "Saint Lucia",
+ "MT-51": "Saint Paul’s Bay",
+ "MT-52": "Sannat",
+ "MT-50": "Saint Lawrence",
+ "MT-48": "Saint Julian",
+ "MT-47": "Safi",
+ "MT-39": "Paola",
+ "MT-45": "Victoria",
+ "MT-44": "Il-Qrendi",
+ "MT-43": "Qormi",
+ "MT-42": "Il-Qala",
+ "MT-41": "Tal-Pieta",
+ "MT-38": "In-Naxxar",
+ "MT-37": "In-Nadur",
+ "MT-36": "Il-Munxar",
+ "MT-34": "L-Imsida",
+ "MT-33": "L-Imqabba",
+ "MT-32": "Il-Mosta",
+ "MT-31": "L-Imgarr",
+ "MT-30": "Il-Mellieha",
+ "MT-29": "L-Imdina",
+ "MT-27": "Marsaskala",
+ "MT-26": "Il-Marsa",
+ "MT-57": "Is-Swieqi",
+ "MT-25": "Luqa",
+ "MT-20": "Senglea",
+ "MT-19": "L-Iklin",
+ "MT-24": "Lija",
+ "MT-22": "Ta' Kercem",
+ "MT-21": "Il-Kalkara",
+ "MT-12": "Il-Gzira",
+ "MT-09": "Floriana",
+ "MT-03": "Il-Birgu",
+ "MT-18": "Il-Hamrun",
+ "MT-11": "Il-Gudja",
+ "MT-17": "Hal Ghaxaq",
+ "MT-16": "L-Ghasri",
+ "MT-15": "Hal Gharghur",
+ "MT-14": "L-Gharb",
+ "MT-13": "Ghajnsielem",
+ "MT-08": "Il-Fgura",
+ "MT-07": "Dingli",
+ "MT-06": "Bormla",
+ "MT-05": "Birzebbuga",
+ "MT-04": "Birkirkara",
+ "MT-02": "Balzan",
+ "MT-01": "Attard",
+ "MT-49": "Saint John",
+ "DZ-50": "Bordj Badji Mokhtar",
+ "SE-C": "Uppsala County",
+ "AT-3": "Lower Austria",
+ "AT-4": "Upper Austria",
+ "AT-5": "Salzburg",
+ "AT-6": "Styria",
+ "DK-81": "North Denmark",
+ "DK-85": "Zealand",
+ "DK-83": "South Denmark",
+ "DK-84": "Capital Region",
+ "DK-82": "Central Jutland",
+ "IS-7": "East",
+ "IS-8": "South",
+ "IS-5": "Northwest",
+ "IS-6": "Northeast",
+ "GB-WLS": "Wales",
+ "GB-ENG": "England",
+ "GB-SCT": "Scotland",
+ "GB-NIR": "Northern Ireland",
+ "IE-U": "Ulster",
+ "CH-BL": "Basel-Landschaft",
+ "CH-BE": "Bern",
+ "CH-SG": "Saint Gallen",
+ "CH-AG": "Aargau",
+ "CH-ZH": "Zurich",
+ "CH-GR": "Grisons",
+ "CH-FR": "Fribourg",
+ "CH-ZG": "Zug",
+ "CH-SO": "Solothurn",
+ "CH-VS": "Valais",
+ "CH-VD": "Vaud",
+ "CH-SZ": "Schwyz",
+ "CH-LU": "Lucerne",
+ "CH-NW": "Nidwalden",
+ "CH-SH": "Schaffhausen",
+ "CH-TG": "Thurgau",
+ "CH-AR": "Appenzell Ausserrhoden",
+ "CH-TI": "Ticino",
+ "CH-JU": "Jura",
+ "CH-GE": "Geneva",
+ "CH-UR": "Uri",
+ "CH-NE": "Neuchâtel",
+ "CH-OW": "Obwalden",
+ "CH-GL": "Glarus",
+ "CH-AI": "Appenzell Innerrhoden",
+ "CH-BS": "Basel-City",
+ "SE-Z": "Jämtland County",
+ "SE-O": "Västra Götaland County",
+ "SE-M": "Skåne County",
+ "SE-F": "Jönköping",
+ "SE-T": "Örebro County",
+ "SE-E": "Östergötland County",
+ "SE-G": "Kronoberg County",
+ "SE-I": "Gotland County",
+ "SE-H": "Kalmar",
+ "SE-U": "Västmanland County",
+ "SE-D": "Södermanland County",
+ "SE-N": "Halland County",
+ "SE-X": "Gävleborg County",
+ "SE-AB": "Stockholm County",
+ "SE-W": "Dalarna County",
+ "SE-S": "Värmland County",
+ "SE-Y": "Västernorrland County",
+ "SE-K": "Blekinge County",
+ "SJ-21": "Svalbard",
+ "PT-18": "Viseu",
+ "PT-03": "Braga",
+ "PT-13": "Porto",
+ "PT-17": "Vila Real",
+ "PT-09": "Guarda",
+ "PT-16": "Viana do Castelo",
+ "PT-06": "Coimbra",
+ "PT-01": "Aveiro",
+ "NL-OV": "Overijssel",
+ "NL-GE": "Gelderland",
+ "NL-DR": "Drenthe",
+ "NL-ZH": "South Holland",
+ "NL-NH": "North Holland",
+ "NL-FR": "Friesland",
+ "NL-NB": "North Brabant",
+ "NL-GR": "Groningen",
+ "NL-ZE": "Zeeland",
+ "NL-UT": "Utrecht",
+ "NL-FL": "Flevoland",
+ "NL-LI": "Limburg",
+ "AT-2": "Carinthia",
+ "AT-1": "Burgenland",
+ "AT-7": "Tyrol",
+ "AT-8": "Vorarlberg",
+ "AT-9": "Vienna",
+ "BE-VLG": "Flanders",
+ "BE-WAL": "Wallonia",
+ "BE-BRU": "Brussels Capital",
+ "DE-SN": "Saxony",
+ "DE-HE": "Hesse",
+ "DE-BY": "Bavaria",
+ "DE-BW": "Baden-Wurttemberg",
+ "DE-RP": "Rheinland-Pfalz",
+ "DE-NW": "North Rhine-Westphalia",
+ "DE-BB": "Brandenburg",
+ "DE-ST": "Saxony-Anhalt",
+ "DE-MV": "Mecklenburg-Vorpommern",
+ "DE-NI": "Lower Saxony",
+ "DE-TH": "Thuringia",
+ "DE-BE": "Land Berlin",
+ "DE-SH": "Schleswig-Holstein",
+ "DE-SL": "Saarland",
+ "DE-HB": "Bremen",
+ "DE-HH": "Hamburg",
+ "LU-WI": "Wiltz",
+ "LU-CL": "Clervaux",
+ "LU-GR": "Grevenmacher",
+ "LU-LU": "Luxembourg",
+ "LU-VD": "Vianden",
+ "LU-ES": "Esch-sur-Alzette",
+ "LU-CA": "Capellen",
+ "LU-DI": "Diekirch",
+ "LU-RM": "Remich",
+ "LU-EC": "Echternach",
+ "LU-ME": "Mersch",
+ "LU-RD": "Redange",
+ "IE-C": "Connacht",
+ "IE-M": "Munster",
+ "IE-L": "Leinster",
+ "FR-NAQ": "Nouvelle-Aquitaine",
+ "FR-HDF": "Hauts-de-France",
+ "FR-GES": "Grand Est",
+ "FR-20R": "Corsica",
+ "FR-CVL": "Centre-Val de Loire",
+ "FR-ARA": "Auvergne-Rhone-Alpes",
+ "FR-PDL": "Pays de la Loire",
+ "FR-BRE": "Brittany",
+ "FR-NOR": "Normandy",
+ "FR-IDF": "Île-de-France",
+ "FR-BFC": "Bourgogne-Franche-Comte",
+ "FR-PAC": "Provence-Alpes-Côte d'Azur",
+ "FR-OCC": "Occitanie",
+ "AD-06": "Sant Julià de Loria",
+ "AD-07": "Andorra la Vella",
+ "AD-03": "Encamp",
+ "AD-05": "Ordino",
+ "AD-08": "Escaldes-Engordany",
+ "AD-04": "La Massana",
+ "AD-02": "Canillo",
+ "LI-11": "Vaduz",
+ "LI-10": "Triesenberg",
+ "LI-09": "Triesen",
+ "LI-08": "Schellenberg",
+ "LI-04": "Mauren",
+ "LI-07": "Schaan",
+ "LI-06": "Ruggell",
+ "LI-05": "Planken",
+ "LI-03": "Gamprin",
+ "LI-02": "Eschen",
+ "LI-01": "Balzers",
+ "HU-BU": "Budapest",
+ "HU-SO": "Somogy megye",
+ "HU-VE": "Veszprem megye",
+ "HU-FE": "Fejér",
+ "HU-ZA": "Zala",
+ "HU-GS": "Győr-Moson-Sopron",
+ "HU-KE": "Komárom-Esztergom",
+ "HU-VA": "Vas",
+ "HU-BA": "Baranya",
+ "HU-NO": "Nograd megye",
+ "HU-TO": "Tolna megye",
+ "SK-ZI": "Zilina",
+ "SK-TC": "Trencin",
+ "SK-NI": "Nitra",
+ "SK-TA": "Trnava",
+ "SK-BL": "Bratislava",
+ "CZ-52": "Kralovehradecky kraj",
+ "CZ-20": "Central Bohemia",
+ "CZ-71": "Olomoucky kraj",
+ "CZ-72": "Zlín",
+ "CZ-64": "South Moravian",
+ "CZ-41": "Karlovarsky kraj",
+ "CZ-31": "Jihocesky kraj",
+ "CZ-42": "Ustecky kraj",
+ "CZ-63": "Kraj Vysocina",
+ "CZ-32": "Plzeň Region",
+ "CZ-80": "Moravskoslezsky kraj",
+ "CZ-51": "Liberecky kraj",
+ "CZ-10": "Prague",
+ "CZ-53": "Pardubicky kraj",
+ "PL-24": "Silesia",
+ "PL-16": "Opole Voivodeship",
+ "PL-32": "West Pomerania",
+ "PL-08": "Lubusz",
+ "PL-22": "Pomerania",
+ "PL-02": "Lower Silesia",
+ "PL-04": "Kujawsko-Pomorskie",
+ "PL-30": "Greater Poland",
+ "ES-PV": "Basque Country",
+ "ES-AR": "Aragon",
+ "ES-GA": "Galicia",
+ "ES-MD": "Madrid",
+ "ES-CL": "Castille and León",
+ "ES-CB": "Cantabria",
+ "ES-CT": "Catalonia",
+ "ES-AS": "Principality of Asturias",
+ "ES-NC": "Navarre",
+ "ES-RI": "La Rioja",
+ "NO-18": "Nordland",
+ "NO-46": "Vestland",
+ "NO-30": "Viken",
+ "NO-15": "Møre og Romsdal",
+ "NO-11": "Rogaland",
+ "NO-34": "Innlandet",
+ "NO-50": "Trøndelag",
+ "NO-42": "Agder",
+ "NO-38": "Vestfold og Telemark",
+ "NO-03": "Oslo County",
+ "IT-34": "Veneto",
+ "IT-45": "Emilia-Romagna",
+ "IT-36": "Friuli Venezia Giulia",
+ "IT-25": "Lombardy",
+ "IT-42": "Liguria",
+ "IT-32": "Trentino-Alto Adige",
+ "IT-62": "Lazio",
+ "IT-21": "Piedmont",
+ "IT-52": "Tuscany",
+ "IT-65": "Abruzzo",
+ "IT-57": "The Marches",
+ "IT-67": "Molise",
+ "IT-55": "Umbria",
+ "SM-09": "Serravalle",
+ "SM-07": "Castello di San Marino Citta",
+ "IT-23": "Aosta Valley",
+ "SM-02": "Chiesanuova",
+ "SM-08": "Castello di Montegiardino",
+ "SM-04": "Castello di Faetano",
+ "SM-06": "Castello di Borgo Maggiore",
+ "SM-01": "Castello di Acquaviva",
+ "AL-11": "Tirana",
+ "AL-01": "Berat County",
+ "AL-10": "Shkodër County",
+ "AL-08": "Lezhë County",
+ "AL-04": "Fier County",
+ "AL-02": "Durrës County",
+ "HR-17": "Split-Dalmatia",
+ "BA-SRP": "Republika Srpska",
+ "SI-193": "Obcina Zuzemberk",
+ "HR-16": "Vukovar-Sirmium",
+ "HR-19": "Dubrovnik-Neretva",
+ "HR-18": "Istria",
+ "HR-02": "County of Krapina-Zagorje",
+ "BA-BIH": "Federation of B&H",
+ "SI-192": "Obcina Zirovnica",
+ "SI-147": "Obcina Ziri",
+ "SI-057": "Obcina Lasko",
+ "SI-135": "Videm",
+ "SI-163": "Jezersko",
+ "SI-207": "Gorje",
+ "SI-204": "Sveta Trojica v Slovenskih Goricah",
+ "SI-113": "Slovenska Bistrica",
+ "SI-055": "Kungota",
+ "SI-052": "Kranj",
+ "SI-191": "Obcina Zetale",
+ "HR-01": "County of Zagreb",
+ "SI-146": "Obcina Zelezniki",
+ "SI-054": "Krsko",
+ "SI-143": "Obcina Zavrc",
+ "SI-190": "Obcina Zalec",
+ "HR-04": "Karlovac",
+ "HR-21": "City of Zagreb",
+ "SI-039": "Obcina Ivancna Gorica",
+ "SI-142": "Zagorje ob Savi",
+ "HR-13": "County of Zadar",
+ "HR-06": "County of Koprivnica-Križevci",
+ "ME-21": "Opstina Zabljak",
+ "SI-141": "Vuzenica",
+ "SI-162": "Horjul",
+ "SI-140": "Vrhnika",
+ "HR-03": "County of Sisak-Moslavina",
+ "SI-019": "Obcina Divaca",
+ "HR-08": "County of Primorje-Gorski Kotar",
+ "HR-20": "County of Međimurje",
+ "SI-189": "Vransko",
+ "SI-201": "Obcina Rence-Vogrsko",
+ "SI-139": "Vojnik",
+ "SI-138": "Vodice",
+ "SI-001": "Obcina Ajdovscina",
+ "HR-15": "Šibenik-Knin",
+ "HR-10": "County of Virovitica-Podravina",
+ "ME-20": "Ulcinj",
+ "SI-182": "Obcina Sveti Andraz v Slovenskih Goricah",
+ "SI-137": "Vitanje",
+ "HR-14": "County of Osijek-Baranja",
+ "SI-136": "Vipava",
+ "SI-206": "Obcina Smarjeske Toplice",
+ "HR-05": "County of Varaždin",
+ "SI-188": "Obcina Verzej",
+ "SI-118": "Obcina Sentilj",
+ "HR-07": "Bjelovar-Bilogora",
+ "SI-130": "Trebnje",
+ "SI-134": "Obcina Velike Lasce",
+ "SI-187": "Velika Polana",
+ "SI-087": "Obcina Ormoz",
+ "SI-032": "Grosuplje",
+ "HR-11": "County of Požega-Slavonia",
+ "SI-085": "Mestna Obcina Novo mesto",
+ "ME-24": "Tuzi",
+ "SI-132": "Obcina Turnisce",
+ "SI-110": "Sevnica",
+ "SI-186": "Trzin",
+ "SI-131": "Obcina Trzic",
+ "SI-185": "Obcina Trnovska vas",
+ "SI-120": "Sentjur",
+ "SI-199": "Mokronog-Trebelno",
+ "SI-129": "Trbovlje",
+ "SI-025": "Dravograd",
+ "SI-205": "Obcina Sveti Tomaz",
+ "SI-128": "Obcina Tolmin",
+ "ME-19": "Tivat",
+ "SI-133": "Velenje",
+ "HR-09": "County of Lika-Senj",
+ "SI-011": "Celje",
+ "SI-184": "Tabor",
+ "SI-181": "Sveta Ana",
+ "SI-094": "Postojna",
+ "SI-144": "Obcina Zrece",
+ "SI-127": "Obcina Store",
+ "SI-115": "Obcina Starse",
+ "HR-12": "Brod-Posavina",
+ "SI-065": "Obcina Loska Dolina",
+ "SI-017": "Obcina Crnomelj",
+ "SI-048": "Obcina Kocevje",
+ "SI-049": "Komen",
+ "SI-043": "Kamnik",
+ "SI-006": "Obcina Bovec",
+ "SI-202": "Obcina Sredisce ob Dravi",
+ "ME-07": "Danilovgrad",
+ "SI-029": "Gornja Radgona",
+ "SI-026": "Duplek",
+ "SI-023": "Obcina Domzale",
+ "SI-058": "Lenart",
+ "SI-036": "Idrija",
+ "SI-159": "Hajdina",
+ "SI-126": "Obcina Sostanj",
+ "SI-180": "Obcina Solcava",
+ "SI-179": "Obcina Sodrazica",
+ "SI-071": "Medvode",
+ "SI-112": "Slovenj Gradec",
+ "SI-194": "Obcina Smartno pri Litiji",
+ "SI-125": "Obcina Smartno ob Paki",
+ "SI-083": "Nazarje",
+ "SI-111": "Obcina Sezana",
+ "SI-124": "Obcina Smarje pri Jelsah",
+ "SI-114": "Slovenske Konjice",
+ "SI-160": "Obcina Hoce-Slivnica",
+ "SI-037": "Ig",
+ "SI-123": "Obcina Skofljica",
+ "SI-122": "Škofja Loka",
+ "SI-050": "Koper",
+ "SI-211": "Obcina Sentrupert",
+ "SI-119": "Obcina Sentjernej",
+ "SI-117": "Obcina Sencur",
+ "SI-183": "Obcina Sempeter-Vrtojba",
+ "SI-084": "Nova Gorica",
+ "SI-109": "Obcina Semic",
+ "SI-090": "Piran",
+ "ME-18": "Opstina Savnik",
+ "SI-033": "Obcina Salovci",
+ "SI-108": "Obcina Ruse",
+ "SI-107": "Rogatec",
+ "SI-105": "Obcina Rogasovci",
+ "SI-106": "Obcina Rogaska Slatina",
+ "SI-044": "Obcina Kanal ob Soci",
+ "ME-10": "Kotor",
+ "SI-177": "Ribnica na Pohorju",
+ "SI-104": "Ribnica",
+ "SI-091": "Pivka",
+ "SI-209": "Obcina Recica ob Savinji",
+ "SI-103": "Obcina Ravne na Koroskem",
+ "SI-013": "Cerknica",
+ "SI-067": "Obcina Luce",
+ "SI-102": "Radovljica",
+ "SI-101": "Radlje ob Dravi",
+ "SI-100": "Radenci",
+ "SI-099": "Obcina Radece",
+ "SI-098": "Obcina Race-Fram",
+ "SI-097": "Puconci",
+ "SI-069": "Obcina Majsperk",
+ "SI-096": "Ptuj",
+ "SI-092": "Obcina Podcetrtek",
+ "SI-175": "Prevalje",
+ "SI-008": "Brezovica",
+ "SI-038": "Ilirska Bistrica",
+ "SI-095": "Preddvor",
+ "SI-174": "Prebold",
+ "SI-173": "Polzela",
+ "SI-060": "Litija",
+ "SI-200": "Obcina Poljcane",
+ "SI-027": "Gorenja Vas-Poljane",
+ "SI-021": "Dobrova-Polhov Gradec",
+ "SI-093": "Podvelka",
+ "SI-051": "Kozje",
+ "SI-082": "Naklo",
+ "SI-172": "Podlehnik",
+ "ME-16": "Podgorica",
+ "ME-15": "Opstina Pluzine",
+ "ME-14": "Pljevlja",
+ "ME-13": "Opstina Plav",
+ "SI-041": "Jesenice",
+ "ME-05": "Budva",
+ "SI-089": "Pesnica",
+ "SI-081": "Muta",
+ "SI-088": "Osilnica",
+ "SI-171": "Oplotnica",
+ "SI-086": "Odranci",
+ "ME-12": "Opstina Niksic",
+ "SI-080": "Murska Sobota",
+ "SI-079": "Mozirje",
+ "SI-078": "Moravske Toplice",
+ "SI-077": "Obcina Moravce",
+ "SI-053": "Kranjska Gora",
+ "ME-11": "Mojkovac",
+ "SI-076": "Mislinja",
+ "SI-170": "Obcina Mirna Pec",
+ "SI-212": "Mirna",
+ "SI-075": "Miren-Kostanjevica",
+ "SI-169": "Obcina Miklavz na Dravskem Polju",
+ "SI-074": "Obcina Mezica",
+ "SI-073": "Metlika",
+ "SI-072": "Obcina Menges",
+ "SI-035": "Hrpelje-Kozina",
+ "SI-070": "Maribor",
+ "SI-198": "Makole",
+ "SI-068": "Lukovica",
+ "SI-167": "Lovrenc na Pohorju",
+ "SI-066": "Obcina Loski Potok",
+ "SI-208": "Log–Dragomer",
+ "SI-063": "Ljutomer",
+ "SI-061": "Ljubljana",
+ "SI-059": "Lendava",
+ "SI-056": "Kuzma",
+ "SI-166": "Obcina Krizevci",
+ "SI-197": "Kostanjevica na Krki",
+ "ME-09": "Opstina Kolasin",
+ "SI-047": "Kobilje",
+ "SI-046": "Obcina Kobarid",
+ "SI-045": "Obcina Kidricevo",
+ "SI-042": "Obcina Jursinci",
+ "SI-210": "Sveti Jurij v Slovenskih Goricah",
+ "SI-116": "Obcina Sveti Jurij ob Scavnici",
+ "SI-040": "Izola",
+ "ME-03": "Berane",
+ "ME-08": "Herceg Novi",
+ "SI-034": "Hrastnik",
+ "SI-064": "Logatec",
+ "SI-161": "Hodos",
+ "ME-22": "Gusinje",
+ "SI-158": "Grad",
+ "SI-031": "Gornji Petrovci",
+ "SI-030": "Gornji Grad",
+ "SI-028": "Obcina Gorisnica",
+ "SI-203": "Obcina Straza",
+ "SI-151": "Obcina Braslovce",
+ "SI-009": "Obcina Brezice",
+ "SI-010": "Obcina Tisina",
+ "SI-022": "Dol pri Ljubljani",
+ "SI-157": "Dolenjske Toplice",
+ "SI-007": "Brda",
+ "SI-156": "Dobrovnik",
+ "SI-155": "Dobrna",
+ "SI-154": "Dobje",
+ "SI-018": "Destrnik",
+ "SI-016": "Obcina Crna na Koroskem",
+ "SI-015": "Obcina Crensovci",
+ "ME-06": "Cetinje",
+ "SI-153": "Cerkvenjak",
+ "SI-012": "Cerklje na Gorenjskem",
+ "SI-152": "Cankova",
+ "BA-BRC": "Brčko",
+ "SI-005": "Borovnica",
+ "SI-004": "Bohinj",
+ "SI-003": "Obcina Bled",
+ "SI-149": "Bistrica ob Sotli",
+ "ME-04": "Bijelo Polje",
+ "SI-148": "Benedikt",
+ "SI-002": "Beltinci",
+ "ME-02": "Bar",
+ "SI-195": "Obcina Apace",
+ "SI-176": "Obcina Razkrizje",
+ "SI-014": "Cerkno",
+ "SI-020": "Dobrepolje",
+ "SI-213": "Ankaran",
+ "ME-23": "Petnjica",
+ "SI-178": "Selnica ob Dravi",
+ "AO-HUI": "Huíla",
+ "AO-CNN": "Cunene Province",
+ "AO-NAM": "Namibe Province",
+ "AO-CCU": "Cuando Cobango",
+ "AO-BGU": "Benguela",
+ "AO-BIE": "Bíe",
+ "AO-HUA": "Huambo",
+ "NA-KH": "Khomas",
+ "NA-OT": "Oshikoto",
+ "NA-ER": "Erongo",
+ "NA-KE": "Kavango East",
+ "NA-KU": "Kunene",
+ "NA-OD": "Otjozondjupa",
+ "NA-ON": "Oshana",
+ "NA-KA": "Karas",
+ "NA-OS": "Omusati",
+ "NA-KW": "Kavango West",
+ "NA-HA": "Hardap",
+ "NA-OW": "Ohangwena",
+ "SH-TA": "Tristan da Cunha",
+ "SH-HL": "Saint Helena",
+ "PT-20": "Azores",
+ "BB-01": "Christ Church",
+ "BB-02": "Saint Andrew",
+ "BB-04": "Saint James",
+ "BB-05": "Saint John",
+ "BB-10": "Saint Philip",
+ "BB-09": "Saint Peter",
+ "BB-08": "Saint Michael",
+ "BB-03": "Saint George",
+ "BB-11": "Saint Thomas",
+ "BB-06": "Saint Joseph",
+ "CV-BR": "Brava",
+ "CV-MA": "Maio",
+ "CV-RB": "Ribeira Brava",
+ "CV-TA": "Tarrafal",
+ "CV-RG": "Ribeira Grande",
+ "CV-TS": "Tarrafal de São Nicolau",
+ "CV-SF": "São Filipe",
+ "CV-SD": "São Domingos",
+ "CV-SL": "Sal",
+ "CV-CR": "Santa Cruz",
+ "CV-BV": "Boa Vista",
+ "CV-PR": "Praia",
+ "CV-PN": "Porto Novo",
+ "CV-MO": "Mosteiros",
+ "CV-PA": "Paul",
+ "CV-SS": "São Salvador do Mundo",
+ "CV-SO": "São Lourenço dos Órgãos",
+ "CV-SV": "São Vicente",
+ "CV-SM": "São Miguel",
+ "CV-CF": "Santa Catarina do Fogo",
+ "CV-RS": "Ribeira Grande de Santiago",
+ "CV-CA": "Santa Catarina",
+ "GY-ES": "Essequibo Islands-West Demerara Region",
+ "GY-EB": "East Berbice-Corentyne Region",
+ "GY-MA": "Mahaica-Berbice Region",
+ "GY-PM": "Pomeroon-Supenaam Region",
+ "GY-PT": "Potaro-Siparuni Region",
+ "GY-DE": "Demerara-Mahaica Region",
+ "GY-BA": "Barima-Waini Region",
+ "GY-UD": "Upper Demerara-Berbice Region",
+ "GY-UT": "Upper Takutu-Upper Essequibo Region",
+ "GY-CU": "Cuyuni-Mazaruni Region",
+ "SR-WA": "Distrikt Wanica",
+ "SR-NI": "Distrikt Nickerie",
+ "SR-CR": "Distrikt Coronie",
+ "SR-PR": "Distrikt Para",
+ "SR-PM": "Distrikt Paramaribo",
+ "SR-CM": "Distrikt Commewijne",
+ "SR-MA": "Distrikt Marowijne",
+ "SR-SA": "Distrikt Saramacca",
+ "SR-BR": "Distrikt Brokopondo",
+ "SR-SI": "Distrikt Sipaliwini",
+ "BR-PB": "Paraíba",
+ "BR-PI": "Piaui",
+ "BR-PE": "Pernambuco",
+ "BR-TO": "Tocantins",
+ "BR-MA": "Maranhao",
+ "BR-PA": "Para",
+ "BR-AP": "Amapa",
+ "BR-CE": "Ceara",
+ "BR-AL": "Alagoas",
+ "BR-AM": "Amazonas",
+ "BR-RN": "Rio Grande do Norte",
+ "BR-BA": "Bahia",
+ "BR-SE": "Sergipe",
+ "BR-RR": "Roraima",
+ "IS-2": "Southern Peninsula",
+ "IS-4": "Westfjords",
+ "IS-3": "West",
+ "IS-1": "Capital Region",
+ "GL-AV": "Avannaata",
+ "GL-QE": "Qeqqata",
+ "GL-QT": "Qeqertalik",
+ "GL-KU": "Kujalleq",
+ "GL-SM": "Sermersooq",
+ "PM-P": "Commune de Saint-Pierre",
+ "PM-M": "Miquelon-Langlade",
+ "AR-B": "Buenos Aires",
+ "AR-N": "Misiones",
+ "AR-C": "Buenos Aires F.D.",
+ "AR-E": "Entre Rios",
+ "AR-S": "Santa Fe",
+ "AR-P": "Formosa",
+ "AR-W": "Corrientes",
+ "AR-H": "Chaco",
+ "PY-6": "Departamento de Caazapa",
+ "PY-11": "Departamento Central",
+ "PY-14": "Departamento de Canindeyu",
+ "PY-4": "Departamento del Guaira",
+ "PY-12": "Departamento de Neembucu",
+ "PY-15": "Departamento de Presidente Hayes",
+ "PY-13": "Departamento del Amambay",
+ "PY-8": "Departamento de Misiones",
+ "PY-10": "Departamento del Alto Parana",
+ "PY-2": "Departamento de San Pedro",
+ "PY-3": "Departamento de la Cordillera",
+ "PY-9": "Departamento de Paraguari",
+ "PY-7": "Departamento de Itapua",
+ "PY-19": "Departamento de Boqueron",
+ "PY-5": "Departamento de Caaguazu",
+ "PY-16": "Departamento de Alto Paraguay",
+ "PY-1": "Departamento de Concepcion",
+ "PY-ASU": "Asuncion",
+ "UY-DU": "Durazno Department",
+ "UY-RV": "Rivera Department",
+ "UY-TT": "Treinta y Tres Department",
+ "UY-FD": "Florida",
+ "UY-CL": "Cerro Largo",
+ "UY-MO": "Montevideo Department",
+ "UY-FS": "Flores Department",
+ "UY-CA": "Canelones",
+ "UY-CO": "Colonia",
+ "UY-TA": "Tacuarembó Department",
+ "UY-SO": "Soriano",
+ "UY-LA": "Lavalleja",
+ "UY-SJ": "San José Department",
+ "UY-MA": "Maldonado",
+ "UY-SA": "Salto Department",
+ "UY-RO": "Rocha Department",
+ "UY-PA": "Paysandú Department",
+ "UY-RN": "Río Negro Department",
+ "UY-AR": "Artigas",
+ "BR-SC": "Santa Catarina",
+ "BR-SP": "Sao Paulo",
+ "BR-PR": "Parana",
+ "BR-RJ": "Rio de Janeiro",
+ "BR-MG": "Minas Gerais",
+ "BR-ES": "Espirito Santo",
+ "BR-RS": "Rio Grande do Sul",
+ "BR-GO": "Goias",
+ "BR-MT": "Mato Grosso",
+ "BR-MS": "Mato Grosso do Sul",
+ "BR-DF": "Federal District",
+ "VE-O": "Nueva Esparta",
+ "VE-A": "Distrito Federal",
+ "VE-G": "Carabobo",
+ "MX-TAM": "Tamaulipas",
+ "MX-SLP": "San Luis Potosí",
+ "VE-B": "Anzoátegui",
+ "VE-E": "Barinas",
+ "JM-03": "Saint Thomas",
+ "JM-07": "Trelawny",
+ "JM-02": "Saint Andrew",
+ "JM-11": "Saint Elizabeth",
+ "JM-06": "Parish of Saint Ann",
+ "JM-14": "Saint Catherine",
+ "JM-13": "Clarendon",
+ "JM-05": "Saint Mary",
+ "JM-04": "Portland",
+ "JM-10": "Westmoreland",
+ "JM-08": "Saint James",
+ "JM-12": "Manchester",
+ "JM-09": "Hanover",
+ "JM-01": "Kingston",
+ "DO-29": "Provincia de Monte Plata",
+ "DO-19": "Provincia de Hermanas Mirabal",
+ "DO-03": "Provincia de Baoruco",
+ "DO-25": "Provincia de Santiago",
+ "DO-21": "Provincia de San Cristobal",
+ "DO-09": "Provincia Espaillat",
+ "DO-11": "Provincia de La Altagracia",
+ "DO-05": "Provincia de Dajabon",
+ "DO-18": "Puerto Plata",
+ "DO-01": "Nacional",
+ "DO-08": "Provincia de El Seibo",
+ "DO-04": "Provincia de Barahona",
+ "DO-20": "Samaná",
+ "DO-23": "Provincia de San Pedro de Macoris",
+ "DO-22": "Provincia de San Juan",
+ "DO-31": "Provincia de San Jose de Ocoa",
+ "DO-32": "Provincia de Santo Domingo",
+ "DO-06": "Provincia Duarte",
+ "DO-15": "Provincia de Monte Cristi",
+ "DO-26": "Provincia de Santiago Rodriguez",
+ "DO-14": "Provincia Maria Trinidad Sanchez",
+ "DO-07": "Provincia de Elias Pina",
+ "DO-16": "Provincia de Pedernales",
+ "DO-02": "Provincia de Azua",
+ "DO-27": "Provincia de Valverde",
+ "DO-28": "Provincia de Monsenor Nouel",
+ "DO-17": "Provincia de Peravia",
+ "DO-13": "Provincia de La Vega",
+ "DO-10": "Provincia de Independencia",
+ "DO-12": "Provincia de La Romana",
+ "DO-24": "Provincia Sanchez Ramirez",
+ "DO-30": "Provincia de Hato Mayor",
+ "BQ-SA": "Saba",
+ "BQ-SE": "Sint Eustatius",
+ "BQ-BO": "Bonaire",
+ "MX-MEX": "México",
+ "MX-GRO": "Guerrero",
+ "MX-VER": "Veracruz",
+ "MX-HID": "Hidalgo",
+ "MX-PUE": "Puebla",
+ "MX-MOR": "Morelos",
+ "MX-OAX": "Oaxaca",
+ "MX-YUC": "Yucatán",
+ "MX-CHP": "Chiapas",
+ "MX-CMX": "Mexico City",
+ "MX-TLA": "Tlaxcala",
+ "MX-TAB": "Tabasco",
+ "MX-ROO": "Quintana Roo",
+ "MX-QUE": "Querétaro",
+ "MX-CAM": "Campeche",
+ "MX-NLE": "Nuevo León",
+ "CU-04": "Matanzas Province",
+ "CU-15": "Artemisa",
+ "CU-13": "Provincia de Santiago de Cuba",
+ "CU-03": "Havana",
+ "CU-10": "Las Tunas",
+ "CU-01": "Provincia de Pinar del Rio",
+ "CU-11": "Holguín Province",
+ "CU-14": "Guantánamo Province",
+ "CU-06": "Cienfuegos Province",
+ "CU-09": "Provincia de Camagueey",
+ "CU-12": "Granma Province",
+ "BS-WG": "West Grand Bahama District",
+ "BS-NE": "North Eleuthera",
+ "BS-SE": "South Eleuthera",
+ "BS-EG": "East Grand Bahama District",
+ "BS-SW": "Spanish Wells District",
+ "BS-NP": "New Providence District",
+ "BS-IN": "Inagua",
+ "BS-CO": "Central Abaco District",
+ "BS-FP": "City of Freeport District",
+ "BS-HI": "Harbour Island",
+ "BS-NO": "North Abaco District",
+ "BS-SS": "San Salvador District",
+ "BS-LI": "Long Island",
+ "BS-BY": "Berry Islands District",
+ "BS-CS": "Central Andros District",
+ "BS-BP": "Black Point District",
+ "BS-NS": "North Andros District",
+ "BM-HA": "Hamilton",
+ "BM-SH": "Southampton Parish",
+ "BM-SA": "Sandys Parish",
+ "BM-GC": "Saint George",
+ "BM-PB": "Pembroke Parish",
+ "BM-PG": "Paget",
+ "BM-SG": "Saint George's Parish",
+ "BM-HC": "Hamilton city",
+ "BM-DS": "Devonshire Parish",
+ "TT-POS": "Port of Spain",
+ "TT-PRT": "Princes Town",
+ "TT-TUP": "Tunapuna/Piarco",
+ "TT-SGE": "Sangre Grande",
+ "TT-TOB": "Tobago",
+ "TT-SIP": "Siparia",
+ "TT-SJL": "San Juan/Laventille",
+ "TT-SFO": "San Fernando",
+ "TT-MRC": "Mayaro",
+ "TT-CTT": "Couva-Tabaquite-Talparo",
+ "TT-PTF": "Point Fortin",
+ "TT-DMN": "Diego Martin",
+ "TT-PED": "Penal/Debe",
+ "TT-CHA": "Chaguanas",
+ "TT-ARI": "Borough of Arima",
+ "KN-15": "Trinity Palmetto Point",
+ "KN-02": "Saint Anne Sandy Point",
+ "KN-13": "Middle Island",
+ "KN-01": "Christ Church Nichola Town",
+ "KN-09": "Saint Paul Capesterre",
+ "KN-05": "Saint James Windward",
+ "KN-11": "Saint Peter Basseterre",
+ "KN-04": "Saint George Gingerland",
+ "KN-12": "Saint Thomas Lowland",
+ "KN-10": "Saint Paul Charlestown",
+ "KN-08": "Saint Mary Cayon",
+ "KN-03": "Saint George Basseterre",
+ "DM-06": "Saint Joseph",
+ "DM-04": "Saint George",
+ "DM-03": "Saint David",
+ "DM-05": "Saint John",
+ "DM-07": "Saint Luke",
+ "DM-02": "Saint Andrew",
+ "DM-11": "Saint Peter",
+ "DM-09": "Saint Patrick",
+ "AG-04": "Parish of Saint John",
+ "AG-03": "Parish of Saint George",
+ "AG-05": "Parish of Saint Mary",
+ "AG-06": "Parish of Saint Paul",
+ "AG-08": "Parish of Saint Philip",
+ "AG-10": "Barbuda",
+ "AG-07": "Parish of Saint Peter",
+ "LC-11": "Vieux-Fort",
+ "LC-08": "Micoud",
+ "LC-02": "Castries",
+ "LC-07": "Laborie",
+ "LC-06": "Gros-Islet",
+ "LC-03": "Choiseul",
+ "LC-12": "Canaries",
+ "VC-04": "Parish of Saint George",
+ "VC-05": "Parish of Saint Patrick",
+ "VC-02": "Parish of Saint Andrew",
+ "VC-06": "Grenadines",
+ "VC-01": "Parish of Charlotte",
+ "VC-03": "Parish of Saint David",
+ "GD-05": "Saint Mark",
+ "GD-03": "Saint George",
+ "GD-06": "Saint Patrick",
+ "GD-02": "Saint David",
+ "GD-10": "Carriacou and Petite Martinique",
+ "GD-01": "Saint Andrew",
+ "GD-04": "Saint John",
+ "BZ-OW": "Orange Walk District",
+ "BZ-CY": "Cayo District",
+ "BZ-BZ": "Belize District",
+ "BZ-TOL": "Toledo District",
+ "BZ-SC": "Stann Creek District",
+ "BZ-CZL": "Corozal District",
+ "SV-LI": "Departamento de La Libertad",
+ "SV-PA": "Departamento de La Paz",
+ "SV-US": "Departamento de Usulutan",
+ "SV-CH": "Departamento de Chalatenango",
+ "SV-SS": "Departamento de San Salvador",
+ "SV-SO": "Departamento de Sonsonate",
+ "SV-SV": "Departamento de San Vicente",
+ "SV-UN": "Departamento de La Union",
+ "SV-SA": "Departamento de Santa Ana",
+ "SV-SM": "Departamento de San Miguel",
+ "SV-CU": "Departamento de Cuscatlan",
+ "SV-AH": "Departamento de Ahuachapan",
+ "SV-MO": "Departamento de Morazan",
+ "GT-ZA": "Zacapa",
+ "GT-GU": "Guatemala",
+ "GT-TO": "Totonicapán",
+ "GT-SO": "Sololá",
+ "GT-SR": "Santa Rosa Department",
+ "GT-SM": "San Marcos",
+ "GT-SA": "Sacatepéquez",
+ "GT-PE": "Petén",
+ "GT-BV": "Baja Verapaz",
+ "GT-QC": "Quiché",
+ "GT-QZ": "Quetzaltenango",
+ "GT-IZ": "Izabal Department",
+ "GT-AV": "Alta Verapaz",
+ "GT-ES": "Departamento de Escuintla",
+ "GT-SU": "Suchitepeque",
+ "GT-JU": "Departamento de Jutiapa",
+ "GT-JA": "Jalapa",
+ "GT-CQ": "Chiquimula",
+ "GT-HU": "Departamento de Huehuetenango",
+ "GT-PR": "El Progreso",
+ "GT-CM": "Chimaltenango",
+ "HN-CR": "Cortés Department",
+ "HN-YO": "Yoro Department",
+ "HN-LE": "Lempira Department",
+ "HN-SB": "Santa Bárbara Department",
+ "HN-IB": "Bay Islands",
+ "HN-CL": "Colón Department",
+ "HN-AT": "Atlántida Department",
+ "HN-FM": "Francisco Morazán Department",
+ "HN-CM": "Comayagua Department",
+ "HN-CP": "Copán Department",
+ "HN-LP": "La Paz Department",
+ "HN-CH": "Choluteca Department",
+ "HN-VA": "Valle Department",
+ "HN-OL": "Olancho Department",
+ "HN-GD": "Gracias a Dios Department",
+ "HN-OC": "Ocotepeque Department",
+ "HN-IN": "Intibucá Department",
+ "HN-EP": "El Paraíso Department",
+ "NI-MN": "Managua Department",
+ "NI-MD": "Madriz Department",
+ "NI-CI": "Departamento de Chinandega",
+ "NI-MT": "Matagalpa Department",
+ "NI-RI": "Departamento de Rivas",
+ "NI-MS": "Masaya Department",
+ "NI-LE": "León Department",
+ "NI-CO": "Chontales Department",
+ "NI-JI": "Jinotega Department",
+ "NI-GR": "Granada Department",
+ "NI-ES": "Estelí Department",
+ "NI-BO": "Boaco Department",
+ "CR-A": "Alajuela Province",
+ "CR-SJ": "Provincia de San Jose",
+ "CR-P": "Puntarenas Province",
+ "CR-H": "Heredia Province",
+ "CR-C": "Cartago Province",
+ "CR-G": "Guanacaste Province",
+ "CR-L": "Limón Province",
+ "VE-V": "Zulia",
+ "VE-U": "Yaracuy",
+ "VE-D": "Aragua",
+ "VE-P": "Portuguesa",
+ "VE-J": "Guárico",
+ "VE-F": "Bolívar",
+ "VE-T": "Estado Trujillo",
+ "VE-Y": "Delta Amacuro",
+ "VE-I": "Falcón",
+ "VE-H": "Cojedes",
+ "VE-S": "Táchira",
+ "VE-K": "Lara",
+ "VE-R": "Sucre",
+ "VE-M": "Miranda",
+ "VE-Z": "Amazonas",
+ "VE-N": "Monagas",
+ "VE-L": "Mérida",
+ "VE-X": "Vargas",
+ "VE-W": "Dependencias Federales",
+ "VE-C": "Apure",
+ "EC-Z": "Zamora Chinchipe",
+ "EC-O": "Provincia de El Oro",
+ "EC-L": "Provincia de Loja",
+ "EC-G": "Provincia del Guayas",
+ "EC-R": "Provincia de Los Rios",
+ "EC-I": "Provincia de Imbabura",
+ "EC-T": "Tungurahua",
+ "EC-P": "Provincia de Pichincha",
+ "EC-C": "Provincia del Carchi",
+ "EC-M": "Provincia de Manabi",
+ "EC-N": "Provincia de Napo",
+ "EC-F": "Canar",
+ "EC-S": "Morona Santiago",
+ "EC-A": "Provincia del Azuay",
+ "EC-U": "Provincia de Sucumbios",
+ "EC-Y": "Pastaza",
+ "EC-X": "Provincia de Cotopaxi",
+ "EC-SD": "Provincia de Santo Domingo de los Tsachilas",
+ "EC-SE": "Provincia de Santa Elena",
+ "EC-E": "Provincia de Esmeraldas",
+ "EC-B": "Provincia de Bolivar",
+ "EC-H": "Chimborazo",
+ "EC-W": "Provincia de Galapagos",
+ "EC-D": "Orellana",
+ "BR-AC": "Acre",
+ "BR-RO": "Rondonia",
+ "CO-CUN": "Cundinamarca",
+ "CO-VAC": "Departamento del Valle del Cauca",
+ "CO-HUI": "Departamento del Huila",
+ "CO-SAN": "Departamento de Santander",
+ "CO-CAS": "Casanare Department",
+ "CO-ANT": "Antioquia",
+ "CO-CAL": "Caldas Department",
+ "CO-MET": "Departamento del Meta",
+ "CO-CAU": "Departamento del Cauca",
+ "CO-NSA": "Norte de Santander Department",
+ "CO-LAG": "La Guajira Department",
+ "CO-ATL": "Atlántico",
+ "CO-BOY": "Departamento de Boyaca",
+ "CO-PUT": "Departamento del Putumayo",
+ "CO-MAG": "Departamento del Magdalena",
+ "CO-SUC": "Departamento de Sucre",
+ "CO-CES": "Departamento del Cesar",
+ "CO-DC": "Bogota D.C.",
+ "CO-AMA": "Amazonas",
+ "CO-BOL": "Departamento de Bolivar",
+ "CO-NAR": "Departamento de Narino",
+ "CO-COR": "Departamento de Cordoba",
+ "CO-ARA": "Departamento de Arauca",
+ "CO-CHO": "Departamento del Choco",
+ "CO-RIS": "Departamento de Risaralda",
+ "CO-TOL": "Departamento de Tolima",
+ "CO-CAQ": "Departamento del Caqueta",
+ "CO-GUA": "Guainía Department",
+ "CO-VID": "Departamento del Vichada",
+ "CO-QUI": "Quindio Department",
+ "CO-VAU": "Departamento del Vaupes",
+ "CO-GUV": "Departamento del Guaviare",
+ "PE-LOR": "Loreto",
+ "PE-ANC": "Ancash",
+ "PE-LAL": "La Libertad",
+ "PE-TUM": "Tumbes",
+ "PE-HUC": "Region de Huanuco",
+ "PE-SAM": "Region de San Martin",
+ "PE-PIU": "Piura",
+ "PE-LAM": "Lambayeque",
+ "PE-UCA": "Ucayali",
+ "PE-AMA": "Amazonas",
+ "PE-CAJ": "Cajamarca",
+ "PA-6": "Provincia de Herrera",
+ "PA-9": "Provincia de Veraguas",
+ "PA-10": "Panamá Oeste Province",
+ "PA-5": "Provincia del Darien",
+ "PA-8": "Provincia de Panama",
+ "PA-3": "Provincia de Colon",
+ "PA-KY": "Guna Yala",
+ "PA-2": "Provincia de Cocle",
+ "PA-1": "Bocas del Toro Province",
+ "PA-NB": "Ngoebe-Bugle",
+ "PA-4": "Chiriquí Province",
+ "HT-SD": "Sud",
+ "HT-AR": "Departement de l'Artibonite",
+ "HT-OU": "Departement de l'Ouest",
+ "HT-NE": "Departement du Nord-Est",
+ "HT-NO": "Nord-Ouest",
+ "HT-CE": "Centre",
+ "HT-NI": "Departement de Nippes",
+ "HT-GA": "Grand'Anse",
+ "HT-ND": "Nord",
+ "BM-SM": "Smith's Parish",
+ "PA-EM": "Embera-Wounaan",
+ "AR-Q": "Neuquen",
+ "AR-Y": "Jujuy",
+ "AR-T": "Tucuman",
+ "AR-L": "La Pampa",
+ "AR-X": "Cordoba",
+ "AR-M": "Mendoza",
+ "AR-F": "La Rioja",
+ "AR-G": "Santiago del Estero",
+ "AR-R": "Rio Negro",
+ "AR-J": "San Juan",
+ "AR-Z": "Santa Cruz",
+ "AR-A": "Salta",
+ "AR-V": "Tierra del Fuego",
+ "AR-U": "Chubut",
+ "AR-K": "Catamarca",
+ "AR-D": "San Luis",
+ "CL-VS": "Region de Valparaiso",
+ "CL-CO": "Coquimbo Region",
+ "CL-BI": "Region del Biobio",
+ "CL-ML": "Maule Region",
+ "CL-LI": "O'Higgins Region",
+ "CL-RM": "Santiago Metropolitan",
+ "CL-AR": "Region de la Araucania",
+ "CL-AT": "Atacama",
+ "CL-LR": "Los Ríos Region",
+ "CL-AN": "Antofagasta",
+ "CL-NB": "Ñuble",
+ "CL-MA": "Region of Magallanes",
+ "CL-LL": "Los Lagos Region",
+ "CL-AP": "Region de Arica y Parinacota",
+ "CL-AI": "Aysén",
+ "CL-TA": "Tarapacá",
+ "BO-T": "Tarija Department",
+ "BO-B": "Beni Department",
+ "BO-H": "Chuquisaca Department",
+ "BO-S": "Santa Cruz Department",
+ "BO-C": "Departamento de Cochabamba",
+ "BO-P": "Potosí Department",
+ "BO-N": "Departamento de Pando",
+ "BO-O": "Oruro",
+ "BO-L": "La Paz Department",
+ "PE-LIM": "Lima region",
+ "PE-CAL": "Callao",
+ "PE-CUS": "Cusco",
+ "PE-PAS": "Pasco",
+ "PE-JUN": "Junin",
+ "PE-PUN": "Puno",
+ "PE-TAC": "Tacna",
+ "PE-ICA": "Ica",
+ "PE-ARE": "Arequipa",
+ "PE-MOQ": "Departamento de Moquegua",
+ "PE-HUV": "Huancavelica",
+ "PE-MDD": "Madre de Dios",
+ "PE-LMA": "Lima",
+ "PE-AYA": "Ayacucho",
+ "PE-APU": "Region de Apurimac",
+ "MX-SON": "Sonora",
+ "MX-ZAC": "Zacatecas",
+ "MX-CHH": "Chihuahua",
+ "MX-SIN": "Sinaloa",
+ "MX-JAL": "Jalisco",
+ "MX-DUR": "Durango",
+ "MX-BCN": "Baja California",
+ "MX-MIC": "Michoacán",
+ "MX-NAY": "Nayarit",
+ "MX-GUA": "Guanajuato",
+ "MX-COA": "Coahuila",
+ "MX-COL": "Colima",
+ "MX-BCS": "Baja California Sur",
+ "MX-AGU": "Aguascalientes",
+ "PF-M": "Iles Marquises",
+ "PF-T": "Iles Tuamotu-Gambier",
+ "KI-L": "Line Islands",
+ "TK-F": "Fakaofo",
+ "TO-04": "Tongatapu",
+ "TO-01": "ʻEua",
+ "TO-05": "Vava'u",
+ "TO-03": "Niuas",
+ "NZ-CIT": "Chatham Islands",
+ "PF-S": "Leeward Islands",
+ "PF-V": "Iles du Vent",
+ "WF-UV": "Uvea",
+ "WF-SG": "Sigave",
+ "WF-AL": "Alo",
+ "WS-VF": "Va'a-o-Fonoti",
+ "WS-PA": "Palauli",
+ "WS-SA": "Satupa'itea",
+ "WS-FA": "Fa'asaleleaga",
+ "WS-GI": "Gagaifomauga",
+ "WS-GE": "Gaga'emauga",
+ "WS-AL": "Aiga-i-le-Tai",
+ "WS-AT": "Atua",
+ "WS-AA": "A'ana",
+ "WS-TU": "Tuamasaga",
+ "FM-YAP": "State of Yap",
+ "PW-224": "State of Ngatpang",
+ "PW-218": "State of Ngarchelong",
+ "MP-R": "Rota",
+ "UM-79": "Wake Island",
+ "US-TX": "Texas",
+ "US-AL": "Alabama",
+ "US-VA": "Virginia",
+ "US-WV": "West Virginia",
+ "US-AR": "Arkansas",
+ "US-DE": "Delaware",
+ "US-FL": "Florida",
+ "US-GA": "Georgia",
+ "US-IL": "Illinois",
+ "US-IN": "Indiana",
+ "US-MD": "Maryland",
+ "US-KS": "Kansas",
+ "US-KY": "Kentucky",
+ "US-MO": "Missouri",
+ "US-NC": "North Carolina",
+ "US-OH": "Ohio",
+ "US-OK": "Oklahoma",
+ "US-SC": "South Carolina",
+ "US-TN": "Tennessee",
+ "US-DC": "District of Columbia",
+ "US-LA": "Louisiana",
+ "US-MS": "Mississippi",
+ "US-NJ": "New Jersey",
+ "US-PA": "Pennsylvania",
+ "VI-T": "Saint Thomas Island",
+ "VI-C": "Saint Croix Island",
+ "VI-J": "Saint John Island",
+ "US-CT": "Connecticut",
+ "US-IA": "Iowa",
+ "US-MA": "Massachusetts",
+ "US-ME": "Maine",
+ "US-MI": "Michigan",
+ "US-MN": "Minnesota",
+ "US-NE": "Nebraska",
+ "US-NY": "New York",
+ "US-SD": "South Dakota",
+ "US-WI": "Wisconsin",
+ "US-ND": "North Dakota",
+ "US-NH": "New Hampshire",
+ "US-RI": "Rhode Island",
+ "US-VT": "Vermont",
+ "US-AZ": "Arizona",
+ "US-CA": "California",
+ "US-NM": "New Mexico",
+ "US-UT": "Utah",
+ "US-CO": "Colorado",
+ "US-NV": "Nevada",
+ "US-ID": "Idaho",
+ "US-AK": "Alaska",
+ "US-MT": "Montana",
+ "US-OR": "Oregon",
+ "US-WA": "Washington",
+ "US-WY": "Wyoming",
+ "US-HI": "Hawaii",
+ "AS-W": "Western District",
+ "AS-M": "Manu'a District",
+ "AS-S": "Swains Island",
+ "AS-E": "Eastern District",
+ "CA-BC": "British Columbia",
+ "CA-NS": "Nova Scotia",
+ "CA-SK": "Saskatchewan",
+ "CA-AB": "Alberta",
+ "CA-NB": "New Brunswick",
+ "CA-ON": "Ontario",
+ "CA-QC": "Quebec",
+ "CA-PE": "Prince Edward Island",
+ "CA-MB": "Manitoba",
+ "CA-NU": "Nunavut",
+ "CA-NL": "Newfoundland and Labrador",
+ "CA-YT": "Yukon",
+ "CA-NT": "Northwest Territories",
+ "ES-CE": "Ceuta",
+ "MM-18": "Nay Pyi Taw",
+ "MY-16": "Putrajaya",
+ "MH-KWA": "Kwajalein Atoll",
+ "NG-BE": "Benue State",
+ "SI-196": "Cirkulane",
+ "SI-164": "Komenda",
+ "WS-VS": "Vaisigano",
+ "MP-T": "Tinian",
+ "PS-JEM": "Quds Governorate",
+ "MH-WTH": "Wotho Atoll",
+ "YE-MA": "Ma’rib",
+ "YE-SD": "Şa‘dah",
+ "TK-N": "Nukunonu",
+ "TK-A": "Atafu",
+ "SJ-22": "Jan Mayen",
+ "KI-P": "Phoenix Islands",
+ "TV-NKL": "Nukulaelae",
+ "TV-NMA": "Nanumea",
+ "TV-NMG": "Nanumanga",
+ "TV-NIT": "Niutao",
+ "TV-NKF": "Nukufetau",
+ "NR-14": "Yaren",
+ "NR-07": "Buada",
+ "KE-02": "Bomet",
+ "PW-370": "State of Sonsorol",
+ "PW-100": "State of Kayangel",
+ "PW-350": "State of Peleliu",
+ "PW-222": "State of Ngardmau",
+ "BW-ST": "Sowa Town",
+ "MP-N": "Northern Islands",
+ "MP-S": "Saipan",
+ "MV-05": "Laamu Atholhu",
+ "MU-CC": "Cargados Carajos",
+ "BW-JW": "Jwaneng",
+ "MT-10": "Il-Fontana",
+ "MT-40": "Pembroke",
+ "SI-165": "Kostel",
+ "TO-02": "Ha'apai",
+ "MK-801": "Aerodrom",
+ "SC-14": "Grand Anse Praslin",
+ "SI-121": "Obcina Skocjan",
+ "MV-02": "Northern Ari Atoll",
+ "MR-15": "Nouakchott Sud",
+ "TD-EE": "Ennedi-Est",
+ "LA-XE": "Khoueng Xekong",
+ "NG-BY": "Bayelsa State",
+ "SC-01": "Anse-aux-Pins",
+ "SC-20": "Pointe Larue",
+ "SC-06": "Baie Lazare",
+ "CU-08": "Ciego de Ávila Province",
+ "MR-13": "Nouakchott Ouest",
+ "SC-15": "La Digue",
+ "SC-18": "Mont Fleuri",
+ "SB-CH": "Choiseul",
+ "KR-50": "Sejong-si",
+ "BS-GC": "Grand Cay District",
+ "SC-03": "Anse Etoile",
+ "SC-07": "Baie Sainte Anne",
+ "SC-09": "Bel Air",
+ "SC-17": "Mont Buxton",
+ "SC-24": "Les Mamelles",
+ "AS-R": "Rose Island",
+ "NR-06": "Boe",
+ "NR-11": "Meneng",
+ "NR-12": "Nibok",
+ "NR-08": "Denigomodu",
+ "GQ-DJ": "Djibloho"
+}
diff --git a/src/Aptabase.csproj b/src/Aptabase.csproj
new file mode 100755
index 0000000..c0faa34
--- /dev/null
+++ b/src/Aptabase.csproj
@@ -0,0 +1,48 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Data/ClickHouseMigrationRunner.cs b/src/Data/ClickHouseMigrationRunner.cs
new file mode 100755
index 0000000..eae95e0
--- /dev/null
+++ b/src/Data/ClickHouseMigrationRunner.cs
@@ -0,0 +1,36 @@
+using Dapper;
+using ClickHouse.Client.ADO;
+using Aptabase.Features;
+
+namespace Aptabase.Data;
+
+public interface IClickHouseMigrationRunner
+{
+ void MigrateUp();
+}
+
+public class ClickHouseMigrationRunner : IClickHouseMigrationRunner
+{
+ private readonly ClickHouseConnection _conn;
+ private readonly EnvSettings _env;
+ private readonly ILogger _logger;
+
+ public ClickHouseMigrationRunner(ClickHouseConnection conn, EnvSettings env, ILogger logger)
+ {
+ _env = env ?? throw new ArgumentNullException(nameof(env));
+ _conn = conn ?? throw new ArgumentNullException(nameof(conn));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public void MigrateUp()
+ {
+ var pathToMigrations = Path.Combine(_env.EtcDirectoryPath, "clickhouse");
+ var files = Directory.GetFiles(pathToMigrations).OrderBy(x => x);
+ foreach (string file in files)
+ {
+ var content = File.ReadAllText(file);
+ _logger.LogDebug($"Executing ClickHouse migration: {file}");
+ _conn.Execute(content);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Data/DbContext.cs b/src/Data/DbContext.cs
new file mode 100755
index 0000000..356f17b
--- /dev/null
+++ b/src/Data/DbContext.cs
@@ -0,0 +1,20 @@
+using System.Data.Common;
+
+namespace Aptabase.Data;
+
+public interface IDbContext
+{
+ DbConnection Connection { get; }
+}
+
+public class DbContext : IDbContext
+{
+ private readonly DbDataSource _ds;
+
+ public DbContext(DbDataSource ds)
+ {
+ _ds = ds ?? throw new ArgumentNullException(nameof(ds));
+ }
+
+ public DbConnection Connection => _ds.CreateConnection();
+}
\ No newline at end of file
diff --git a/src/Data/Migrations/0001_InitialSetup.cs b/src/Data/Migrations/0001_InitialSetup.cs
new file mode 100755
index 0000000..f3eeb6a
--- /dev/null
+++ b/src/Data/Migrations/0001_InitialSetup.cs
@@ -0,0 +1,44 @@
+using FluentMigrator;
+
+namespace Aptabase.Data.Migrations;
+
+[Migration(0001)]
+public class InitialSetup : Migration
+{
+ public override void Up()
+ {
+ Create.Table("apps")
+ .WithNanoIdColumn("id").PrimaryKey()
+ .WithNanoIdColumn("owner_id")
+ .WithColumn("name").AsString(40).NotNullable()
+ .WithColumn("app_key").AsString(20).NotNullable()
+ .WithColumn("deleted_at").AsDateTime().Nullable()
+ .WithTimestamps();
+
+ Create.Index("idx_apps_owner")
+ .OnTable("apps")
+ .OnColumn("owner_id").Ascending();
+
+ Create.Index("idx_apps_key")
+ .OnTable("apps")
+ .OnColumn("app_key").Ascending()
+ .WithOptions().Unique();
+
+ Create.Table("users")
+ .WithNanoIdColumn("id").PrimaryKey()
+ .WithColumn("name").AsString(40).NotNullable()
+ .WithColumn("email").AsString(300).NotNullable()
+ .WithTimestamps();
+
+ Create.Index("idx_users_email")
+ .OnTable("users")
+ .OnColumn("email")
+ .Unique();
+ }
+
+ public override void Down()
+ {
+ Delete.Table("apps");
+ Delete.Table("users");
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Migrations/0002_UserProvider.cs b/src/Data/Migrations/0002_UserProvider.cs
new file mode 100755
index 0000000..0a9084e
--- /dev/null
+++ b/src/Data/Migrations/0002_UserProvider.cs
@@ -0,0 +1,21 @@
+using FluentMigrator;
+
+namespace Aptabase.Data.Migrations;
+
+[Migration(0002)]
+public class UserProvider : Migration
+{
+ public override void Up()
+ {
+ Create.Table("user_providers")
+ .WithColumn("provider_name").AsString(40).NotNullable().PrimaryKey()
+ .WithColumn("provider_uid").AsString(40).NotNullable().PrimaryKey()
+ .WithNanoIdColumn("user_id").ForeignKey("users", "id")
+ .WithTimestamps();
+ }
+
+ public override void Down()
+ {
+ Delete.Table("user_providers");
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Migrations/0003_AddBlobs.cs b/src/Data/Migrations/0003_AddBlobs.cs
new file mode 100755
index 0000000..8dd4280
--- /dev/null
+++ b/src/Data/Migrations/0003_AddBlobs.cs
@@ -0,0 +1,25 @@
+using FluentMigrator;
+
+namespace Aptabase.Data.Migrations;
+
+[Migration(0003)]
+public class AddBlobs : Migration
+{
+ public override void Up()
+ {
+ Create.Table("blobs")
+ .WithColumn("path").AsString(80).NotNullable().PrimaryKey()
+ .WithColumn("content").AsBinary().NotNullable()
+ .WithColumn("content_type").AsString(80).NotNullable()
+ .WithTimestamps();
+
+ Alter.Table("apps")
+ .AddColumn("icon_path").AsString(80).Nullable();
+ }
+
+ public override void Down()
+ {
+ Delete.Column("icon_path").FromTable("apps");
+ Delete.Table("blobs");
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Migrations/0004_AddAppSalt.cs b/src/Data/Migrations/0004_AddAppSalt.cs
new file mode 100755
index 0000000..77c500f
--- /dev/null
+++ b/src/Data/Migrations/0004_AddAppSalt.cs
@@ -0,0 +1,21 @@
+using FluentMigrator;
+
+namespace Aptabase.Data.Migrations;
+
+[Migration(0004)]
+public class AddAppSalt : Migration
+{
+
+ public override void Up()
+ {
+ Create.Table("app_salts")
+ .WithNanoIdColumn("app_id").ForeignKey("apps", "id").PrimaryKey()
+ .WithColumn("date").AsString(10).NotNullable().PrimaryKey()
+ .WithColumn("salt").AsBinary(16).NotNullable();
+ }
+
+ public override void Down()
+ {
+ Delete.Table("app_salts");
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Migrations/0005_AppShares.cs b/src/Data/Migrations/0005_AppShares.cs
new file mode 100755
index 0000000..0dbe605
--- /dev/null
+++ b/src/Data/Migrations/0005_AppShares.cs
@@ -0,0 +1,21 @@
+using FluentMigrator;
+
+namespace Aptabase.Data.Migrations;
+
+[Migration(0005)]
+public class AppShares : Migration
+{
+
+ public override void Up()
+ {
+ Create.Table("app_shares")
+ .WithNanoIdColumn("app_id").ForeignKey("apps", "id").PrimaryKey()
+ .WithColumn("email").AsString(300).NotNullable().PrimaryKey()
+ .WithTimestamps();
+ }
+
+ public override void Down()
+ {
+ Delete.Table("app_shares");
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Migrations/0006_AddSubscriptions.cs b/src/Data/Migrations/0006_AddSubscriptions.cs
new file mode 100755
index 0000000..4711048
--- /dev/null
+++ b/src/Data/Migrations/0006_AddSubscriptions.cs
@@ -0,0 +1,30 @@
+using FluentMigrator;
+
+namespace Aptabase.Data.Migrations;
+
+[Migration(0006)]
+public class AddSubscriptions : Migration
+{
+ public override void Up()
+ {
+ Create.Table("subscriptions")
+ .WithColumn("id").AsInt64().NotNullable().PrimaryKey()
+ .WithNanoIdColumn("owner_id").NotNullable()
+ .WithColumn("customer_id").AsInt64().NotNullable()
+ .WithColumn("product_id").AsInt64().NotNullable()
+ .WithColumn("variant_id").AsInt64().NotNullable()
+ .WithColumn("status").AsString().NotNullable()
+ .WithColumn("ends_at").AsDateTimeOffset().Nullable()
+ .WithTimestamps();
+
+ Create.Index("idx_subscriptions_owner")
+ .OnTable("subscriptions")
+ .OnColumn("owner_id");
+ }
+
+ public override void Down()
+ {
+ Delete.Index("idx_subscriptions_owner");
+ Delete.Table("billing");
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Migrations/0007_AddOnboardingFlag.cs b/src/Data/Migrations/0007_AddOnboardingFlag.cs
new file mode 100755
index 0000000..d74a3ee
--- /dev/null
+++ b/src/Data/Migrations/0007_AddOnboardingFlag.cs
@@ -0,0 +1,23 @@
+using FluentMigrator;
+
+namespace Aptabase.Data.Migrations;
+
+[Migration(0007)]
+public class AddOnboardingFlag : Migration
+{
+ public override void Up()
+ {
+ Alter.Table("apps")
+ .AddColumn("has_events").AsBoolean().Nullable();
+
+ Execute.Sql("UPDATE apps SET has_events = true");
+
+ Alter.Table("apps")
+ .AlterColumn("has_events").AsBoolean().NotNullable();
+ }
+
+ public override void Down()
+ {
+ Delete.Column("has_events").FromTable("apps");
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Migrations/0008_AddLockReason.cs b/src/Data/Migrations/0008_AddLockReason.cs
new file mode 100755
index 0000000..d300f0f
--- /dev/null
+++ b/src/Data/Migrations/0008_AddLockReason.cs
@@ -0,0 +1,18 @@
+using FluentMigrator;
+
+namespace Aptabase.Data.Migrations;
+
+[Migration(0008)]
+public class AddLockReason : Migration
+{
+ public override void Up()
+ {
+ Alter.Table("users")
+ .AddColumn("lock_reason").AsFixedLengthString(1).Nullable();
+ }
+
+ public override void Down()
+ {
+ Delete.Column("lock_reason").FromTable("users");
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Migrations/0009_AddFreeTrial.cs b/src/Data/Migrations/0009_AddFreeTrial.cs
new file mode 100755
index 0000000..f26237a
--- /dev/null
+++ b/src/Data/Migrations/0009_AddFreeTrial.cs
@@ -0,0 +1,20 @@
+using FluentMigrator;
+
+namespace Aptabase.Data.Migrations;
+
+[Migration(0009)]
+public class AddFreeTrial : Migration
+{
+ public override void Up()
+ {
+ Alter.Table("users")
+ .AddColumn("free_trial_ends_at").AsDateTimeOffset().Nullable()
+ .AddColumn("free_quota").AsInt64().Nullable();
+ }
+
+ public override void Down()
+ {
+ Delete.Column("free_trial_ends_at").FromTable("users");
+ Delete.Column("free_quota").FromTable("users");
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Migrations/0010_AddCache.cs b/src/Data/Migrations/0010_AddCache.cs
new file mode 100755
index 0000000..8b83fac
--- /dev/null
+++ b/src/Data/Migrations/0010_AddCache.cs
@@ -0,0 +1,20 @@
+using FluentMigrator;
+
+namespace Aptabase.Data.Migrations;
+
+[Migration(0010)]
+public class AddCache : Migration
+{
+ public override void Up()
+ {
+ Create.Table("cache")
+ .WithColumn("key").AsString(100).NotNullable().PrimaryKey()
+ .WithColumn("value").AsCustom("TEXT").NotNullable()
+ .WithColumn("expires_at").AsDateTimeOffset().NotNullable();
+ }
+
+ public override void Down()
+ {
+ Delete.Table("cache");
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Migrations/0011_AddFeatureFlags.cs b/src/Data/Migrations/0011_AddFeatureFlags.cs
new file mode 100755
index 0000000..29ee597
--- /dev/null
+++ b/src/Data/Migrations/0011_AddFeatureFlags.cs
@@ -0,0 +1,31 @@
+using FluentMigrator;
+
+namespace Aptabase.Data.Migrations;
+
+[Migration(0011)]
+public class AddFeatureFlags : Migration
+{
+ public override void Up()
+ {
+ Create.Table("feature_flags")
+ .WithNanoIdColumn("id").PrimaryKey()
+ .WithNanoIdColumn("app_id").ForeignKey("apps", "id")
+ .WithColumn("key").AsString(255).NotNullable()
+ .WithColumn("value").AsString(4000).NotNullable()
+ .WithColumn("environment").AsString(50).NotNullable()
+ .WithColumn("conditions").AsString(4000).NotNullable()
+ .WithTimestamps();
+
+ Create.Index("idx_feature_flags_app_key_env")
+ .OnTable("feature_flags")
+ .OnColumn("app_id").Ascending()
+ .OnColumn("key").Ascending()
+ .OnColumn("environment").Ascending();
+ }
+
+ public override void Down()
+ {
+ Delete.Index("idx_feature_flags_app_key_env");
+ Delete.Table("feature_flags");
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Migrations/MigrationExtensions.cs b/src/Data/Migrations/MigrationExtensions.cs
new file mode 100755
index 0000000..e0aa9d6
--- /dev/null
+++ b/src/Data/Migrations/MigrationExtensions.cs
@@ -0,0 +1,22 @@
+using FluentMigrator;
+using FluentMigrator.Builders.Create.Table;
+
+namespace Aptabase.Data.Migrations;
+
+internal static class MigrationExtensions
+{
+ public static ICreateTableColumnOptionOrWithColumnSyntax WithNanoIdColumn(this ICreateTableWithColumnSyntax tableWithColumnSyntax, string name)
+ {
+ return tableWithColumnSyntax
+ .WithColumn(name)
+ .AsString(22)
+ .NotNullable();
+ }
+
+ public static ICreateTableColumnOptionOrWithColumnSyntax WithTimestamps(this ICreateTableWithColumnSyntax tableWithColumnSyntax)
+ {
+ return tableWithColumnSyntax
+ .WithColumn("created_at").AsDateTimeOffset().WithDefault(SystemMethods.CurrentUTCDateTime).NotNullable()
+ .WithColumn("modified_at").AsDateTimeOffset().WithDefault(SystemMethods.CurrentUTCDateTime).NotNullable();
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Migrations/VersionInfoTable.cs b/src/Data/Migrations/VersionInfoTable.cs
new file mode 100755
index 0000000..4e6fa74
--- /dev/null
+++ b/src/Data/Migrations/VersionInfoTable.cs
@@ -0,0 +1,17 @@
+using FluentMigrator.Runner.VersionTableInfo;
+
+namespace Aptabase.Data.Migrations;
+
+[VersionTableMetaData]
+public class VersionTable : IVersionTableMetaData
+{
+ public string ColumnName => "version";
+ public string SchemaName => "";
+ public string TableName => "migration_history";
+ public string UniqueIndexName => "idx_version";
+ public string AppliedOnColumnName => "applied_at";
+ public string DescriptionColumnName => "description";
+ public object? ApplicationContext { get; set; }
+ public bool OwnsSchema => true;
+ public bool CreateWithPrimaryKey => false;
+}
\ No newline at end of file
diff --git a/src/Data/NanoId.cs b/src/Data/NanoId.cs
new file mode 100755
index 0000000..5830e5b
--- /dev/null
+++ b/src/Data/NanoId.cs
@@ -0,0 +1,13 @@
+using NanoidDotNet;
+
+namespace Aptabase.Data;
+
+public class NanoId
+{
+ private const string ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ private const string NUMBERS = "0123456789";
+
+ public static string New(string preffix) => $"{preffix}{Nanoid.Generate(ALPHABET, 22 - preffix.Length)}";
+ public static string New() => New("");
+ public static string Numbers(int len) => Nanoid.Generate(NUMBERS, len);
+}
\ No newline at end of file
diff --git a/src/Extensions/ExceptionMiddleware.cs b/src/Extensions/ExceptionMiddleware.cs
new file mode 100755
index 0000000..7d98477
--- /dev/null
+++ b/src/Extensions/ExceptionMiddleware.cs
@@ -0,0 +1,57 @@
+// Custom Exception Middleware to catch common exceptions caused by client disconnects
+// Some conditions can be removed on .NET 8 because of https://github.com/dotnet/aspnetcore/pull/46330
+using System.Net;
+
+public class ExceptionMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+
+ public ExceptionMiddleware(RequestDelegate next, ILogger logger)
+ {
+ _next = next;
+ _logger = logger;
+ }
+
+ public async Task Invoke(HttpContext context)
+ {
+ try
+ {
+ await _next(context);
+ }
+ catch (TaskCanceledException) when (context.RequestAborted.IsCancellationRequested)
+ {
+ context.Response.StatusCode = 418; // I'm a teapot
+ }
+ catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
+ {
+ context.Response.StatusCode = 418; // I'm a teapot
+ }
+ catch (BadHttpRequestException ex) when (context.RequestAborted.IsCancellationRequested || ex.StatusCode == (int)HttpStatusCode.RequestTimeout)
+ {
+ context.Response.StatusCode = 418; // I'm a teapot
+ }
+#pragma warning disable CS0618 // Type or member is obsolete
+ catch (Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException)
+ {
+ context.Response.StatusCode = 418; // I'm a teapot
+ }
+#pragma warning restore CS0618 // Type or member is obsolete
+ catch (HttpRequestException ex)
+ {
+ context.Response.StatusCode = (int)(ex.StatusCode ?? HttpStatusCode.InternalServerError);
+
+ _logger.LogError(ex, "Dependency error on {Path}", context.Request.Path.Value);
+ }
+ catch (Microsoft.AspNetCore.Authentication.AuthenticationFailureException)
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
+ }
+ catch (Exception ex)
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
+
+ _logger.LogError(ex, "Unexpected error on {Path}", context.Request.Path.Value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Extensions/HttpExtensions.cs b/src/Extensions/HttpExtensions.cs
new file mode 100755
index 0000000..071c13a
--- /dev/null
+++ b/src/Extensions/HttpExtensions.cs
@@ -0,0 +1,36 @@
+namespace Microsoft.AspNetCore.Http;
+
+public static class HttpContextExtensions
+{
+ public static string ResolveClientIpAddress(this HttpContext httpContext)
+ {
+ if (httpContext.Request.Headers.TryGetValue("X-Real-Ip", out var ip) && !string.IsNullOrEmpty(ip))
+ return ip.ToString();
+
+ if (httpContext.Request.Headers.TryGetValue("X-Forwarded-For", out var forwardedIp) && !string.IsNullOrEmpty(forwardedIp))
+ return forwardedIp.ToString();
+
+ var cfViewerAddress = httpContext.Request.Headers["CloudFront-Viewer-Address"];
+ if (cfViewerAddress.Count > 0)
+ {
+ var parts = (cfViewerAddress[0] ?? string.Empty).Split(":");
+ if (parts.Length == 1)
+ return parts[0];
+
+ if (parts.Length >= 1)
+ return string.Join(":", parts[0..^1]);
+ }
+
+ return httpContext.Connection.RemoteIpAddress?.ToString() ?? "";
+ }
+
+ public static async Task EnsureSuccessWithLog(this HttpResponseMessage response, ILogger logger)
+ {
+ if (!response.IsSuccessStatusCode)
+ {
+ var responseBody = await response.Content.ReadAsStringAsync();
+ logger.LogError("Request failed with {StatusCode} with body {ResponseBody}", response.StatusCode, responseBody);
+ response.EnsureSuccessStatusCode();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Extensions/StringExtensions.cs b/src/Extensions/StringExtensions.cs
new file mode 100755
index 0000000..074fb93
--- /dev/null
+++ b/src/Extensions/StringExtensions.cs
@@ -0,0 +1,15 @@
+namespace System;
+
+public static class StringExtensions
+{
+ public static string Truncate(this string value, int maxLength, string suffix = "...")
+ {
+ if (string.IsNullOrEmpty(value))
+ return value;
+
+ if (value.Length <= maxLength)
+ return value;
+
+ return $"{value[..(maxLength - suffix.Length)]}{suffix}";
+ }
+}
\ No newline at end of file
diff --git a/src/Extensions/TelemetryExtensions.cs b/src/Extensions/TelemetryExtensions.cs
new file mode 100755
index 0000000..4b30af6
--- /dev/null
+++ b/src/Extensions/TelemetryExtensions.cs
@@ -0,0 +1,108 @@
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+
+namespace Microsoft.Extensions.Hosting;
+
+// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
+// This project should be referenced by each service project in your solution.
+// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
+public static class Extensions
+{
+ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
+ {
+ builder.ConfigureOpenTelemetry();
+
+ builder.AddDefaultHealthChecks();
+
+ builder.Services.AddServiceDiscovery();
+
+ builder.Services.ConfigureHttpClientDefaults(http =>
+ {
+ // Turn on resilience by default
+ http.AddStandardResilienceHandler();
+
+ // Turn on service discovery by default
+ http.AddServiceDiscovery();
+ });
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
+ {
+ builder.Logging.AddOpenTelemetry(logging =>
+ {
+ logging.IncludeFormattedMessage = true;
+ logging.IncludeScopes = true;
+ });
+
+ builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics =>
+ {
+ metrics.AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation();
+ })
+ .WithTracing(tracing =>
+ {
+ tracing.AddAspNetCoreInstrumentation()
+ // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
+ //.AddGrpcClientInstrumentation()
+ .AddHttpClientInstrumentation();
+ });
+
+ builder.AddOpenTelemetryExporters();
+
+ return builder;
+ }
+
+ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
+ {
+ var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+
+ if (useOtlpExporter)
+ {
+ builder.Services.AddOpenTelemetry().UseOtlpExporter();
+ }
+
+ // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
+ //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
+ //{
+ // builder.Services.AddOpenTelemetry()
+ // .UseAzureMonitor();
+ //}
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
+ {
+ builder.Services.AddHealthChecks()
+ // Add a default liveness check to ensure app is responsive
+ .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
+
+ return builder;
+ }
+
+ public static WebApplication MapDefaultEndpoints(this WebApplication app)
+ {
+ // Adding health checks endpoints to applications in non-development environments has security implications.
+ // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
+ if (app.Environment.IsDevelopment())
+ {
+ // All health checks must pass for app to be considered ready to accept traffic after starting
+ app.MapHealthChecks("/health");
+
+ // Only health checks tagged with the "live" tag must pass for app to be considered alive
+ app.MapHealthChecks("/alive", new HealthCheckOptions
+ {
+ Predicate = r => r.Tags.Contains("live")
+ });
+ }
+
+ return app;
+ }
+}
diff --git a/src/Features/Apps/AppQueries.cs b/src/Features/Apps/AppQueries.cs
new file mode 100755
index 0000000..d153cf4
--- /dev/null
+++ b/src/Features/Apps/AppQueries.cs
@@ -0,0 +1,74 @@
+using Aptabase.Data;
+using Dapper;
+
+namespace Aptabase.Features.Apps;
+
+public class Application
+{
+ public string Id { get; set; } = "";
+ public string Name { get; set; } = "";
+ public string AppKey { get; set; } = "";
+ public string IconPath { get; set; } = "";
+ public bool HasEvents { get; set; } = false;
+ public bool HasOwnership { get; set; } = false;
+ public char? LockReason { get; set; }
+}
+
+public interface IAppQueries
+{
+ Task GetActiveAppByAppKey(string appKey, CancellationToken cancellationToken);
+ Task MaskAsOnboarded(string appId, CancellationToken cancellationToken);
+ Task GetOwnedAppAsync(string appId, string userId);
+}
+
+public class AppQueries : IAppQueries
+{
+ private readonly IDbContext _db;
+
+ public AppQueries(IDbContext db)
+ {
+ _db = db ?? throw new ArgumentNullException(nameof(db));
+ }
+
+ public Task GetActiveAppByAppKey(string appKey, CancellationToken cancellationToken)
+ {
+ var cmd = new CommandDefinition(@"
+ SELECT a.id, a.name, a.icon_path,
+ a.app_key, a.has_events,
+ u.lock_reason
+ FROM apps a
+ INNER JOIN users u
+ ON u.id = a.owner_id
+ WHERE a.app_key = @appKey AND a.deleted_at IS NULL",
+ new { appKey },
+ cancellationToken: cancellationToken
+ );
+
+ return _db.Connection.QueryFirstOrDefaultAsync(cmd);
+ }
+
+ public async Task MaskAsOnboarded(string appId, CancellationToken cancellationToken)
+ {
+ var cmd = new CommandDefinition($"UPDATE apps SET has_events = true WHERE id = @appId",
+ new { appId },
+ cancellationToken: cancellationToken
+ );
+
+ await _db.Connection.ExecuteAsync(cmd);
+ }
+
+ public async Task GetOwnedAppAsync(string appId, string userId)
+ {
+ return await _db.Connection.QueryFirstOrDefaultAsync(
+ @"SELECT a.id, a.name, a.icon_path, a.app_key, true as has_ownership,
+ a.has_events, u.lock_reason
+ FROM apps a
+ INNER JOIN users u
+ ON u.id = a.owner_id
+ WHERE a.id = @appId
+ AND a.owner_id = @userId
+ AND a.deleted_at IS NULL",
+ new { appId, userId }
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Apps/AppsController.cs b/src/Features/Apps/AppsController.cs
new file mode 100755
index 0000000..0655fab
--- /dev/null
+++ b/src/Features/Apps/AppsController.cs
@@ -0,0 +1,222 @@
+using Dapper;
+using Aptabase.Data;
+using Aptabase.Features.Blob;
+using Microsoft.AspNetCore.Mvc;
+using Aptabase.Features.Authentication;
+using System.ComponentModel.DataAnnotations;
+
+namespace Aptabase.Features.Apps;
+
+public class ApplicationShare
+{
+ public string Email { get; set; } = "";
+ public DateTime CreatedAt { get; set; }
+}
+
+public class CreateAppRequestBody
+{
+ [Required]
+ [StringLength(40, MinimumLength = 2)]
+ public string Name { get; set; } = "";
+}
+
+public class UpdateAppRequestBody
+{
+ public string Icon { get; set; } = "";
+
+ [Required]
+ [StringLength(40, MinimumLength = 2)]
+ public string Name { get; set; } = "";
+}
+
+[ApiController, IsAuthenticated]
+[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
+public class AppsController : Controller
+{
+ private readonly IDbContext _db;
+ private readonly EnvSettings _env;
+ private readonly IBlobService _blobService;
+
+ public AppsController(IDbContext db, EnvSettings env, IBlobService blobService)
+ {
+ _db = db ?? throw new ArgumentNullException(nameof(db));
+ _env = env ?? throw new ArgumentNullException(nameof(env));
+ _blobService = blobService ?? throw new ArgumentNullException(nameof(blobService));
+ }
+
+ [HttpGet("/api/_apps")]
+ public async Task ListApps()
+ {
+ var user = this.GetCurrentUserIdentity();
+
+ var apps = await _db.Connection.QueryAsync(
+ @"SELECT a.id, a.name, a.icon_path, a.app_key,
+ a.owner_id = @userId AS has_ownership, a.has_events,
+ u.lock_reason
+ FROM apps a
+ LEFT JOIN app_shares s
+ ON s.app_id = a.id
+ INNER JOIN users u
+ ON u.id = a.owner_id
+ WHERE (a.owner_id = @userId OR s.email = @userEmail)
+ AND a.deleted_at IS NULL
+ GROUP BY a.id, a.name, a.icon_path, a.app_key, u.lock_reason
+ ORDER by a.name", new { userId = user.Id, userEmail = user.Email });
+
+ return Ok(apps);
+ }
+
+ [HttpPost("/api/_apps")]
+ public async Task Create([FromBody] CreateAppRequestBody body)
+ {
+ var user = this.GetCurrentUserIdentity();
+ var app = new Application
+ {
+ Id = NanoId.New(),
+ Name = body.Name,
+ AppKey = $"A-{_env.Region}-{NanoId.Numbers(10)}"
+ };
+
+ await _db.Connection.ExecuteScalarAsync(@"
+ INSERT INTO apps (id, owner_id, name, app_key, has_events)
+ VALUES (@appId, @ownerId, @name, @appKey, false)",
+ new
+ {
+ appId = app.Id,
+ ownerId = user.Id,
+ name = app.Name,
+ appKey = app.AppKey
+ });
+
+ return Ok(app);
+ }
+
+ [HttpGet("/api/_apps/{appId}")]
+ public async Task GetAppById(string appId)
+ {
+ var user = this.GetCurrentUserIdentity();
+
+ var app = await _db.Connection.QueryFirstOrDefaultAsync(
+ @"SELECT a.id, a.name, a.icon_path, a.app_key,
+ a.owner_id = @userId as has_ownership, a.has_events,
+ u.lock_reason
+ FROM apps a
+ LEFT JOIN app_shares s
+ ON s.app_id = a.id
+ INNER JOIN users u
+ ON u.id = a.owner_id
+ WHERE a.id = @appId
+ AND (a.owner_id = @userId OR s.email = @userEmail)
+ AND a.deleted_at IS NULL
+ GROUP BY a.id, a.name, a.icon_path, a.app_key, u.lock_reason
+ ORDER by a.name",
+ new { appId, userId = user.Id, userEmail = user.Email }
+ );
+
+ return Ok(app);
+ }
+
+ [HttpPut("/api/_apps/{appId}")]
+ public async Task Update(string appId, [FromBody] UpdateAppRequestBody body, CancellationToken cancellationToken)
+ {
+ var app = await GetOwnedApp(appId);
+ if (app == null)
+ return NotFound();
+
+ if (!string.IsNullOrEmpty(body.Icon))
+ {
+ var content = Convert.FromBase64String(body.Icon);
+ app.IconPath = await _blobService.UploadAsync("icons", content, "image/png", cancellationToken);
+ }
+
+ app.Name = body.Name;
+ await _db.Connection.ExecuteScalarAsync("UPDATE apps SET name = @name, icon_path = @iconPath WHERE id = @appId", new
+ {
+ appId = app.Id,
+ name = app.Name,
+ iconPath = app.IconPath,
+ });
+
+ return Ok(app);
+ }
+
+ [HttpDelete("/api/_apps/{appId}")]
+ public async Task Delete(string appId)
+ {
+ var app = await GetOwnedApp(appId);
+ if (app == null)
+ return NotFound();
+
+ await _db.Connection.ExecuteScalarAsync("UPDATE apps SET deleted_at = now() WHERE id = @appId", new
+ {
+ appId = app.Id,
+ });
+
+ return Ok(new { });
+ }
+
+ [HttpGet("/api/_apps/{appId}/shares")]
+ public async Task ListAppShares(string appId)
+ {
+ var app = await GetOwnedApp(appId);
+ if (app == null)
+ return NotFound();
+
+ var shares = await _db.Connection.QueryAsync(
+ @"SELECT email, created_at
+ FROM app_shares
+ WHERE app_id = @appId", new { appId });
+ return Ok(shares);
+ }
+
+ [HttpPut("/api/_apps/{appId}/shares/{email}")]
+ public async Task ShareApp(string appId, string email)
+ {
+ var app = await GetOwnedApp(appId);
+ if (app == null)
+ return NotFound();
+
+ await _db.Connection.ExecuteScalarAsync(@"
+ INSERT INTO app_shares (app_id, email)
+ VALUES (@appId, @email)
+ ON CONFLICT DO NOTHING", new
+ {
+ appId,
+ email = email.ToLower(),
+ });
+
+ return Ok(new { });
+ }
+
+ [HttpDelete("/api/_apps/{appId}/shares/{email}")]
+ public async Task UnshareApp(string appId, string email)
+ {
+ var app = await GetOwnedApp(appId);
+ if (app == null)
+ return NotFound();
+
+ await _db.Connection.ExecuteScalarAsync(@"DELETE FROM app_shares WHERE app_id = @appId AND email = @email", new
+ {
+ appId,
+ email,
+ });
+
+ return Ok(new { });
+ }
+
+ private async Task GetOwnedApp(string appId)
+ {
+ var user = this.GetCurrentUserIdentity();
+ return await _db.Connection.QueryFirstOrDefaultAsync(
+ @"SELECT a.id, a.name, a.icon_path, a.app_key, true as has_ownership,
+ a.has_events, u.lock_reason
+ FROM apps a
+ INNER JOIN users u
+ ON u.id = a.owner_id
+ WHERE a.id = @appId
+ AND a.owner_id = @userId
+ AND a.deleted_at IS NULL",
+ new { appId, userId = user.Id }
+ );
+ }
+}
diff --git a/src/Features/Authentication/AuthController.cs b/src/Features/Authentication/AuthController.cs
new file mode 100755
index 0000000..d81c7ef
--- /dev/null
+++ b/src/Features/Authentication/AuthController.cs
@@ -0,0 +1,140 @@
+using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Cors;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.RateLimiting;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Aptabase.Features.Authentication;
+
+public class SignInBodyRequest
+{
+ [EmailAddress]
+ public string Email { get; set; } = "";
+}
+
+public class RegisterBodyRequest
+{
+ [StringLength(40, MinimumLength = 2)]
+ public string Name { get; set; } = "";
+
+ [EmailAddress]
+ public string Email { get; set; } = "";
+}
+
+[ApiController]
+[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
+public class AuthController : Controller
+{
+ private readonly ILogger _logger;
+ private readonly EnvSettings _env;
+ private readonly IAuthService _authService;
+ private readonly IAuthTokenManager _tokenManager;
+
+ public AuthController(
+ ILogger logger,
+ EnvSettings env,
+ IAuthService authService,
+ IAuthTokenManager tokenManager)
+ {
+ _env = env ?? throw new ArgumentNullException(nameof(env));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _authService = authService ?? throw new ArgumentNullException(nameof(authService));
+ _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager));
+ }
+
+ [HttpPost("/api/_auth/signin")]
+ public async Task SignIn([FromBody] SignInBodyRequest body, CancellationToken cancellationToken)
+ {
+ var found = await _authService.SendSignInEmailAsync(body.Email.Trim(), cancellationToken);
+
+ if (!found)
+ return NotFound(new { });
+
+ return Ok(new { });
+ }
+
+ [HttpPost("/api/_auth/register")]
+ [EnableRateLimiting("SignUp")]
+ public async Task Register([FromBody] RegisterBodyRequest body, CancellationToken cancellationToken)
+ {
+ await _authService.SendRegisterEmailAsync(body.Name.Trim(), body.Email.Trim(), cancellationToken);
+ return Ok(new { });
+ }
+
+ [HttpGet("/api/_auth/github")]
+ public IActionResult GitHub()
+ {
+ return Challenge(new AuthenticationProperties { RedirectUri = $"{_env.SelfBaseUrl}/" }, "github");
+ }
+
+ [HttpGet("/api/_auth/google")]
+ public IActionResult Google()
+ {
+ return Challenge(new AuthenticationProperties { RedirectUri = $"{_env.SelfBaseUrl}/" }, "google");
+ }
+
+ [HttpGet("/api/_auth/me")]
+ [IsAuthenticated]
+ [EnableCors("AllowAptabaseCom")]
+ public async Task Me(CancellationToken cancellationToken)
+ {
+ var identity = this.GetCurrentUserIdentity();
+ var user = await _authService.FindUserByIdAsync(identity.Id, cancellationToken);
+ if (user is null)
+ return NotFound();
+
+ return Ok(user);
+ }
+
+ [HttpPost("/api/_auth/account/delete")]
+ [IsAuthenticated]
+ [EnableCors("AllowAptabaseCom")]
+ public async Task DeleteAccount(CancellationToken cancellationToken)
+ {
+ var identity = this.GetCurrentUserIdentity();
+ var user = await _authService.FindUserByIdAsync(identity.Id, cancellationToken);
+ if (user is null)
+ return NotFound();
+
+ await _authService.DeleteUserByIdAsync(identity.Id, cancellationToken);
+
+ return Ok(user);
+ }
+
+ [HttpPost("/api/_auth/signout")]
+ public async Task ForceSignOut()
+ {
+ await _authService.SignOutAsync();
+ return Redirect($"{_env.SelfBaseUrl}/auth");
+ }
+
+ [HttpGet("/api/_auth/continue")]
+ public async Task HandleMagicLink([FromQuery] string token, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var result = _tokenManager.ParseAuthToken(token);
+ var user = await _authService.FindUserByEmailAsync(result.Email, cancellationToken);
+
+ if (result.Type == AuthTokenType.Register && user == null)
+ user = await _authService.CreateAccountAsync(result.Name, result.Email, cancellationToken);
+
+ if (user != null)
+ await _authService.SignInAsync(user);
+ else
+ _logger.LogError("Tried to authenticate user with email {email}, but account was not found", result.Email);
+ }
+ catch (SecurityTokenExpiredException)
+ {
+ return Redirect($"{_env.SelfBaseUrl}/auth?error=expired");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Unable to validate auth token");
+ return Redirect($"{_env.SelfBaseUrl}/auth?error=invalid");
+ }
+
+ return Redirect($"{_env.SelfBaseUrl}/");
+ }
+}
diff --git a/src/Features/Authentication/AuthExtensions.cs b/src/Features/Authentication/AuthExtensions.cs
new file mode 100755
index 0000000..4a5e892
--- /dev/null
+++ b/src/Features/Authentication/AuthExtensions.cs
@@ -0,0 +1,27 @@
+using Aptabase.Features.Authentication;
+
+namespace Microsoft.AspNetCore.Mvc;
+
+public static class AuthExtensions
+{
+ public static bool IsAuthenticated(this HttpContext context)
+ {
+ return context.User.Identity?.IsAuthenticated ?? false;
+ }
+
+ public static UserIdentity GetCurrentUserIdentity(this Controller controller)
+ {
+ return controller.HttpContext.GetCurrentUserIdentity();
+ }
+
+ public static UserIdentity GetCurrentUserIdentity(this HttpContext context)
+ {
+ if (!context.IsAuthenticated())
+ throw new InvalidOperationException("User is not authenticated.");
+
+ var id = context.User.FindFirst(x => x.Type == "id")?.Value ?? "";
+ var email = context.User.FindFirst(x => x.Type == "email")?.Value ?? "";
+ var name = context.User.FindFirst(x => x.Type == "name")?.Value ?? "";
+ return new UserIdentity(id, name, email);
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Authentication/AuthService.cs b/src/Features/Authentication/AuthService.cs
new file mode 100755
index 0000000..7fe3689
--- /dev/null
+++ b/src/Features/Authentication/AuthService.cs
@@ -0,0 +1,221 @@
+using Aptabase.Features.Notification;
+using Aptabase.Data;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using System.Security.Claims;
+using Dapper;
+
+namespace Aptabase.Features.Authentication;
+
+public interface IAuthService
+{
+ Task SendSignInEmailAsync(string email, CancellationToken cancellationToken);
+ Task SendRegisterEmailAsync(string name, string email, CancellationToken cancellationToken);
+ Task SignInAsync(UserAccount user);
+ Task SignOutAsync();
+ Task FindUserByIdAsync(string id, CancellationToken cancellationToken);
+ Task FindUserByEmailAsync(string email, CancellationToken cancellationToken);
+ Task FindUserByOAuthProviderAsync(string providerName, string providerUid, CancellationToken cancellationToken);
+ Task FindOrCreateAccountWithOAuthAsync(string name, string email, string providerName, string providerUid, CancellationToken cancellationToken);
+ Task CreateAccountAsync(string name, string email, CancellationToken cancellationToken);
+ Task AttachUserAuthProviderAsync(UserAccount user, string providerName, string providerUid, CancellationToken cancellationToken);
+ Task DeleteUserByIdAsync(string id, CancellationToken cancellationToken);
+}
+
+public class AuthService : IAuthService
+{
+ private readonly ILogger _logger;
+ private readonly IDbContext _db;
+ private readonly IAuthTokenManager _tokenManager;
+ private readonly EnvSettings _env;
+ private readonly IEmailClient _emailClient;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
+ public AuthService(
+ ILogger logger,
+ IDbContext db,
+ EnvSettings env,
+ IHttpContextAccessor httpContextAccessor,
+ IEmailClient emailClient,
+ IAuthTokenManager tokenManager)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
+ _env = env ?? throw new ArgumentNullException(nameof(env));
+ _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager));
+ _emailClient = emailClient ?? throw new ArgumentNullException(nameof(emailClient));
+ _db = db ?? throw new ArgumentNullException(nameof(db)); ;
+ }
+
+ public async Task SendSignInEmailAsync(string email, CancellationToken cancellationToken)
+ {
+ var user = await FindUserByEmailAsync(email, cancellationToken);
+ if (user != null)
+ {
+ var token = _tokenManager.CreateAuthToken(AuthTokenType.SignIn, user.Name, user.Email);
+ await _emailClient.SendEmailAsync(email, "Log in to Aptabase", "SignIn", new()
+ {
+ { "name", user.Name },
+ { "url", GenerateAuthUrl(token) }
+ }, cancellationToken);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public async Task SendRegisterEmailAsync(string name, string email, CancellationToken cancellationToken)
+ {
+ var user = await FindUserByEmailAsync(email, cancellationToken);
+ if (user != null)
+ {
+ await SendSignInEmailAsync(email, cancellationToken);
+ return;
+ }
+
+ var token = _tokenManager.CreateAuthToken(AuthTokenType.Register, name, email);
+
+ await _emailClient.SendEmailAsync(email, "Confirm your registration", "Register", new()
+ {
+ { "name", name },
+ { "url", GenerateAuthUrl(token) }
+ }, cancellationToken);
+ }
+
+ public async Task CreateAccountAsync(string name, string email, CancellationToken cancellationToken)
+ {
+ var userId = NanoId.New();
+ var cmd = new CommandDefinition(
+ "INSERT INTO users (id, name, email, free_quota) VALUES (@userId, @name, @email, 20000)",
+ new { userId, name, email = email.ToLower() },
+ cancellationToken: cancellationToken
+ );
+
+ await _db.Connection.ExecuteAsync(cmd);
+
+ return new UserAccount(new UserIdentity(userId, name, email));
+ }
+
+ public async Task SignOutAsync()
+ {
+ if (_httpContextAccessor.HttpContext == null)
+ return;
+
+ await _httpContextAccessor.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ }
+
+ public async Task SignInAsync(UserAccount user)
+ {
+ if (_httpContextAccessor.HttpContext == null)
+ {
+ _logger.LogError("Tried to authenticate without an active HTTP Context");
+ return;
+ }
+
+ var claims = new List
+ {
+ new("id", user.Id),
+ new("name", user.Name),
+ new("email", user.Email),
+ };
+
+ var claimsIdentity = new ClaimsIdentity(
+ claims, CookieAuthenticationDefaults.AuthenticationScheme);
+
+ var authProperties = new AuthenticationProperties
+ {
+ ExpiresUtc = DateTime.UtcNow.AddDays(365),
+ IsPersistent = true,
+ };
+
+ await _httpContextAccessor.HttpContext.SignInAsync(
+ CookieAuthenticationDefaults.AuthenticationScheme,
+ new ClaimsPrincipal(claimsIdentity),
+ authProperties);
+ }
+
+ public Task DeleteUserByIdAsync(string id, CancellationToken cancellationToken)
+ {
+ var sql = @"
+ BEGIN;
+ DELETE FROM public.app_shares
+ WHERE app_id IN (SELECT id FROM public.apps WHERE owner_id = @userId);
+
+ DELETE FROM public.app_salts
+ WHERE app_id IN (SELECT id FROM public.apps WHERE owner_id = @userId);
+
+ DELETE FROM public.apps
+ WHERE owner_id = @userId;
+
+ DELETE FROM public.user_providers
+ WHERE user_id = @userId;
+
+ DELETE FROM public.subscriptions
+ WHERE owner_id = @userId;
+
+ DELETE FROM public.users
+ WHERE id = @userId;
+ COMMIT;";
+
+ var cmd = new CommandDefinition(
+ sql,
+ new { userId = id },
+ cancellationToken: cancellationToken
+ );
+
+ return _db.Connection.ExecuteAsync(cmd);
+ }
+
+ public async Task FindUserByIdAsync(string id, CancellationToken cancellationToken)
+ {
+ var cmd = new CommandDefinition($"SELECT id, name, email, lock_reason FROM users WHERE id = @id", new { id }, cancellationToken: cancellationToken);
+ return await _db.Connection.QuerySingleOrDefaultAsync(cmd);
+ }
+
+ public async Task FindUserByEmailAsync(string email, CancellationToken cancellationToken)
+ {
+ var cmd = new CommandDefinition($"SELECT id, name, email, lock_reason FROM users WHERE email = @email", new { email = email.ToLower() }, cancellationToken: cancellationToken);
+ return await _db.Connection.QuerySingleOrDefaultAsync(cmd);
+ }
+
+ public async Task FindOrCreateAccountWithOAuthAsync(string name, string email, string providerName, string providerUid, CancellationToken cancellationToken)
+ {
+ var user = await FindUserByOAuthProviderAsync(providerName, providerUid, cancellationToken);
+ if (user is not null)
+ return user;
+
+ user = await FindUserByEmailAsync(email, cancellationToken);
+ if (user is not null)
+ {
+ await AttachUserAuthProviderAsync(user, providerName, providerUid, cancellationToken);
+ return user;
+ }
+
+ user = await CreateAccountAsync(name, email, cancellationToken);
+ await AttachUserAuthProviderAsync(user, providerName, providerUid, cancellationToken);
+ return user;
+ }
+
+ public async Task FindUserByOAuthProviderAsync(string providerName, string providerUid, CancellationToken cancellationToken)
+ {
+ var cmd = new CommandDefinition($@"
+ SELECT u.id, u.name, u.email, u.lock_reason
+ FROM user_providers up
+ INNER JOIN users u
+ ON u.id = up.user_id
+ WHERE up.provider_name = @name
+ AND up.provider_uid = @uid", new { name = providerName, uid = providerUid }, cancellationToken: cancellationToken);
+ return await _db.Connection.QuerySingleOrDefaultAsync(cmd);
+ }
+
+ public Task AttachUserAuthProviderAsync(UserAccount user, string providerName, string providerUid, CancellationToken cancellationToken)
+ {
+ var cmd = new CommandDefinition($@"
+ INSERT INTO user_providers (provider_name, provider_uid, user_id)
+ VALUES (@providerName, @providerUid, @userId)", new { userId = user.Id, providerName, providerUid }, cancellationToken: cancellationToken);
+ return _db.Connection.ExecuteAsync(cmd);
+ }
+
+ private string GenerateAuthUrl(string token) => $"{_env.SelfBaseUrl}/api/_auth/continue?token={token}";
+}
\ No newline at end of file
diff --git a/src/Features/Authentication/AuthTokenManager.cs b/src/Features/Authentication/AuthTokenManager.cs
new file mode 100755
index 0000000..05ae8bb
--- /dev/null
+++ b/src/Features/Authentication/AuthTokenManager.cs
@@ -0,0 +1,84 @@
+using Microsoft.IdentityModel.Tokens;
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+
+namespace Aptabase.Features.Authentication;
+
+public enum AuthTokenType
+{
+ SignIn,
+ Register
+}
+
+public class ParsedAuthToken
+{
+ public AuthTokenType Type { get; set; }
+ public string Name { get; set; } = "";
+ public string Email { get; set; } = "";
+}
+
+public interface IAuthTokenManager
+{
+ string CreateAuthToken(AuthTokenType type, string name, string email);
+ ParsedAuthToken ParseAuthToken(string token);
+}
+
+public class AuthTokenManager : IAuthTokenManager
+{
+ private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
+ private readonly SymmetricSecurityKey _signingKey;
+ private readonly string _issuer;
+
+ public AuthTokenManager(EnvSettings env)
+ {
+ _issuer = $"aptabase-{env.Region.ToLower()}";
+ _signingKey = new SymmetricSecurityKey(env.AuthSecret);
+ }
+
+ public string CreateAuthToken(AuthTokenType type, string name, string email)
+ {
+ var claims = new[]
+ {
+ new Claim("type", type.ToString()),
+ new Claim("name", name),
+ new Claim("email", email)
+ };
+ var tokenDescriptor = new SecurityTokenDescriptor
+ {
+ Issuer = _issuer,
+ Subject = new ClaimsIdentity(claims),
+ Expires = DateTime.UtcNow.Add(TimeSpan.FromMinutes(15)),
+ SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256)
+ };
+
+ var token = _tokenHandler.CreateToken(tokenDescriptor);
+ return _tokenHandler.WriteToken(token);
+ }
+
+ public ParsedAuthToken ParseAuthToken(string token)
+ {
+ if (string.IsNullOrEmpty(token))
+ throw new ArgumentNullException(nameof(token));
+
+ _tokenHandler.ValidateToken(token, new TokenValidationParameters
+ {
+ ValidAlgorithms = new[] { SecurityAlgorithms.HmacSha256 },
+ ValidIssuer = _issuer,
+ ValidateIssuer = true,
+ IssuerSigningKey = _signingKey,
+ ValidateIssuerSigningKey = true,
+ ValidateAudience = false,
+ ValidateLifetime = true,
+ ClockSkew = TimeSpan.Zero,
+ }, out SecurityToken validatedToken);
+
+ var jwtToken = (JwtSecurityToken)validatedToken;
+
+ return new ParsedAuthToken
+ {
+ Type = (AuthTokenType)Enum.Parse(typeof(AuthTokenType), jwtToken.Claims.First(x => x.Type == "type").Value),
+ Name = jwtToken.Claims.First(x => x.Type == "name").Value,
+ Email = jwtToken.Claims.First(x => x.Type == "email").Value
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Authentication/IsAuthenticated.cs b/src/Features/Authentication/IsAuthenticated.cs
new file mode 100755
index 0000000..7af7a72
--- /dev/null
+++ b/src/Features/Authentication/IsAuthenticated.cs
@@ -0,0 +1,18 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+
+namespace Aptabase.Features.Authentication;
+
+public class IsAuthenticatedAttribute : ActionFilterAttribute
+{
+ public override void OnActionExecuting(ActionExecutingContext context)
+ {
+ if (!context.HttpContext.IsAuthenticated())
+ {
+ context.Result = new UnauthorizedResult();
+ return;
+ }
+
+ base.OnActionExecuting(context);
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Authentication/OAuthExtensions.cs b/src/Features/Authentication/OAuthExtensions.cs
new file mode 100755
index 0000000..51ada58
--- /dev/null
+++ b/src/Features/Authentication/OAuthExtensions.cs
@@ -0,0 +1,164 @@
+using System.Net.Http.Headers;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Aptabase.Features;
+using Aptabase.Features.Authentication;
+using Microsoft.AspNetCore.Authentication.OAuth;
+
+namespace Microsoft.AspNetCore.Authentication;
+
+public static class OAuthExtensions
+{
+ public class GitHubUser
+ {
+ [JsonPropertyName("id")]
+ public long Id { get; set; }
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = "";
+ [JsonPropertyName("login")]
+ public string Login { get; set; } = "";
+ [JsonPropertyName("email")]
+ public string Email { get; set; } = "";
+ }
+
+ public class GitHubEmail
+ {
+ [JsonPropertyName("email")]
+ public string Email { get; set; } = "";
+ [JsonPropertyName("primary")]
+ public bool Primary { get; set; }
+ [JsonPropertyName("verified")]
+ public bool Verified { get; set; }
+ }
+
+ public class GoogleUser
+ {
+ [JsonPropertyName("sub")]
+ public string Id { get; set; } = "";
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = "";
+ [JsonPropertyName("email")]
+ public string Email { get; set; } = "";
+ [JsonPropertyName("email_verified")]
+ public bool EmailVerified { get; set; }
+ }
+
+ public static AuthenticationBuilder AddGitHub(this AuthenticationBuilder builder, EnvSettings env)
+ {
+ if (string.IsNullOrWhiteSpace(env.OAuthGitHubClientId))
+ return builder;
+
+ return builder.AddOAuth("github", o =>
+ {
+ o.ClientId = env.OAuthGitHubClientId;
+ o.ClientSecret = env.OAuthGitHubClientSecret;
+ o.CallbackPath = new PathString("/api/_auth/github/callback");
+ o.Scope.Add("read:user");
+ o.Scope.Add("user:email");
+ o.CorrelationCookie.SameSite = env.IsDevelopment ? SameSiteMode.Unspecified : SameSiteMode.None;
+ o.CorrelationCookie.HttpOnly = true;
+ o.CorrelationCookie.IsEssential = true;
+ o.CorrelationCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
+ o.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
+ o.TokenEndpoint = "https://github.com/login/oauth/access_token";
+ o.UserInformationEndpoint = "https://api.github.com/user";
+ o.ClaimActions.MapJsonKey("id", "id");
+ o.ClaimActions.MapJsonKey("name", "name");
+ o.ClaimActions.MapJsonKey("email", "email");
+ o.Events = new OAuthEvents
+ {
+ OnAccessDenied = context =>
+ {
+ context.HandleResponse();
+ context.HttpContext.Response.Redirect($"{env.SelfBaseUrl}/auth");
+ return Task.CompletedTask;
+ },
+ OnCreatingTicket = async context =>
+ {
+ var ghUser = await MakeOAuthRequest(context, context.Options.UserInformationEndpoint);
+ if (ghUser is null)
+ throw new Exception("Failed to retrieve GitHub user information.");
+
+ if (string.IsNullOrWhiteSpace(ghUser.Name))
+ ghUser.Name = ghUser.Login;
+
+ if (string.IsNullOrWhiteSpace(ghUser.Email))
+ ghUser.Email = await GetGitHubPreferredEmail(context);
+
+ if (string.IsNullOrWhiteSpace(ghUser.Email))
+ throw new Exception("Could not find a verified email, can't login with GitHub.");
+
+ var authService = context.HttpContext.RequestServices.GetRequiredService();
+ var user = await authService.FindOrCreateAccountWithOAuthAsync(ghUser.Name, ghUser.Email, "github", ghUser.Id.ToString(), context.HttpContext.RequestAborted);
+ context.RunClaimActions(JsonSerializer.SerializeToElement(new { id = user.Id, name = user.Name, email = user.Email }));
+ }
+ };
+ });
+ }
+
+ public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, EnvSettings env)
+ {
+ if (string.IsNullOrWhiteSpace(env.OAuthGoogleClientId))
+ return builder;
+
+ return builder.AddOAuth("google", o =>
+ {
+ o.ClientId = env.OAuthGoogleClientId;
+ o.ClientSecret = env.OAuthGoogleClientSecret;
+ o.Scope.Add("openid");
+ o.Scope.Add("profile");
+ o.Scope.Add("email");
+ o.CallbackPath = new PathString("/api/_auth/google/callback");
+ o.CorrelationCookie.SameSite = env.IsDevelopment ? SameSiteMode.Unspecified : SameSiteMode.None;
+ o.CorrelationCookie.HttpOnly = true;
+ o.CorrelationCookie.IsEssential = true;
+ o.CorrelationCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
+ o.AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth";
+ o.TokenEndpoint = "https://oauth2.googleapis.com/token";
+ o.UserInformationEndpoint = "https://www.googleapis.com/oauth2/v3/userinfo";
+ o.ClaimActions.MapJsonKey("id", "id");
+ o.ClaimActions.MapJsonKey("name", "name");
+ o.ClaimActions.MapJsonKey("email", "email");
+ o.Events = new OAuthEvents
+ {
+ OnAccessDenied = context =>
+ {
+ context.HandleResponse();
+ context.HttpContext.Response.Redirect($"{env.SelfBaseUrl}/auth");
+ return Task.CompletedTask;
+ },
+ OnCreatingTicket = async context =>
+ {
+ var googleUser = await MakeOAuthRequest(context, context.Options.UserInformationEndpoint);
+ if (googleUser is null)
+ throw new Exception("Failed to retrieve Google user information.");
+
+ if (!googleUser.EmailVerified)
+ throw new Exception("Email not verified, can't login with Google.");
+
+ var authService = context.HttpContext.RequestServices.GetRequiredService();
+ var user = await authService.FindOrCreateAccountWithOAuthAsync(googleUser.Name, googleUser.Email, "google", googleUser.Id, context.HttpContext.RequestAborted);
+ context.RunClaimActions(JsonSerializer.SerializeToElement(new { id = user.Id, name = user.Name, email = user.Email }));
+ }
+ };
+ });
+ }
+
+ private static async Task MakeOAuthRequest(OAuthCreatingTicketContext context, string endpoint)
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
+ using var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
+
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadFromJsonAsync();
+ }
+
+ private static async Task GetGitHubPreferredEmail(OAuthCreatingTicketContext context)
+ {
+ var emails = await MakeOAuthRequest(context, "https://api.github.com/user/emails");
+ return emails?.Where(e => e.Verified).OrderBy(e => e.Primary ? 0 : 1).FirstOrDefault()?.Email ?? "";
+ }
+
+}
\ No newline at end of file
diff --git a/src/Features/Authentication/UserAccount.cs b/src/Features/Authentication/UserAccount.cs
new file mode 100755
index 0000000..94132d9
--- /dev/null
+++ b/src/Features/Authentication/UserAccount.cs
@@ -0,0 +1,36 @@
+using System.Text;
+using System.Security.Cryptography;
+
+namespace Aptabase.Features.Authentication;
+
+public class UserAccount
+{
+ public string Id { get; set; } = "";
+ public string Name { get; set; } = "";
+ public string Email { get; set; } = "";
+ public string AvatarUrl => GetAvatarUrl();
+ public char? LockReason { get; set; }
+
+ public UserAccount()
+ {
+
+ }
+
+ public UserAccount(UserIdentity identity)
+ {
+ Id = identity.Id;
+ Name = identity.Name;
+ Email = identity.Email;
+ }
+
+ private static MD5 md5 = MD5.Create();
+ private string GetAvatarUrl()
+ {
+ var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(Email.Trim().ToLower()));
+ var sb = new StringBuilder();
+ foreach (var b in hash)
+ sb.Append(b.ToString("x2"));
+
+ return $"https://md.sudovanilla.org/images/icons/Aptabase.jpg";
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Authentication/UserIdentifier.cs b/src/Features/Authentication/UserIdentifier.cs
new file mode 100755
index 0000000..e1cbfbe
--- /dev/null
+++ b/src/Features/Authentication/UserIdentifier.cs
@@ -0,0 +1,15 @@
+namespace Aptabase.Features.Authentication;
+
+public struct UserIdentity
+{
+ public string Id { get; private set; } = "";
+ public string Name { get; private set; } = "";
+ public string Email { get; private set; } = "";
+
+ public UserIdentity(string id, string name, string email)
+ {
+ Id = id;
+ Name = name;
+ Email = email;
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Billing/BillingController.cs b/src/Features/Billing/BillingController.cs
new file mode 100755
index 0000000..20f272b
--- /dev/null
+++ b/src/Features/Billing/BillingController.cs
@@ -0,0 +1,93 @@
+using Aptabase.Features.Authentication;
+using Aptabase.Features.Billing.LemonSqueezy;
+using Aptabase.Features.Stats;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Aptabase.Features.Billing;
+
+[ApiController, IsAuthenticated]
+[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
+public class BillingController : Controller
+{
+ private readonly IQueryClient _queryClient;
+ private readonly IBillingQueries _billingQueries;
+ private readonly LemonSqueezyClient _lsClient;
+
+ public BillingController(IBillingQueries billingQueries, LemonSqueezyClient lsClient, IQueryClient queryClient)
+ {
+ _billingQueries = billingQueries ?? throw new ArgumentNullException(nameof(billingQueries));
+ _lsClient = lsClient ?? throw new ArgumentNullException(nameof(lsClient));
+ _queryClient = queryClient ?? throw new ArgumentNullException(nameof(queryClient));
+ }
+
+ [HttpGet("/api/_billing")]
+ public async Task BillingState(CancellationToken cancellationToken)
+ {
+ var user = this.GetCurrentUserIdentity();
+ var appIds = await _billingQueries.GetOwnedAppIds(user);
+
+ var usage = await _queryClient.NamedQuerySingleAsync("get_billing_usage__v1", new {
+ app_ids = appIds,
+ }, cancellationToken);
+
+ var sub = await _billingQueries.GetUserSubscription(user);
+ var plan = sub is null || sub.Status == "expired"
+ ? SubscriptionPlan.GetFreeVariant(await _billingQueries.GetUserFreeTierOrTrial(user))
+ : SubscriptionPlan.GetByVariantId(sub.VariantId);
+
+ var state = (usage?.Count ?? 0) < plan.MonthlyEvents ? "OK" : "OVERUSE";
+
+ return Ok(new
+ {
+ State = state,
+ Usage = usage?.Count ?? 0,
+ DateTime.UtcNow.Month,
+ DateTime.UtcNow.Year,
+ Subscription = sub != null ? new
+ {
+ sub.Status,
+ sub.EndsAt,
+ } : null,
+ Plan = plan
+ });
+ }
+
+ [HttpGet("/api/_billing/historical")]
+ public async Task HistoricalUsage(CancellationToken cancellationToken)
+ {
+ var user = this.GetCurrentUserIdentity();
+ var appIds = await _billingQueries.GetOwnedAppIds(user);
+
+ var rows = await _queryClient.NamedQueryAsync("billing_historical_usage__v1", new
+ {
+ app_ids = appIds,
+ }, cancellationToken);
+
+ return Ok(rows);
+ }
+
+ [HttpPost("/api/_billing/checkout")]
+ public async Task GenerateCheckoutUrl(CancellationToken cancellationToken)
+ {
+ var user = this.GetCurrentUserIdentity();
+ var url = await _lsClient.CreateCheckout(user, cancellationToken);
+
+ return Ok(new { url });
+ }
+
+ [HttpPost("/api/_billing/portal")]
+ public async Task GeneratePortalUrl(CancellationToken cancellationToken)
+ {
+ var user = this.GetCurrentUserIdentity();
+ var sub = await _billingQueries.GetUserSubscription(user);
+
+ if (sub is null)
+ {
+ return NotFound();
+ }
+
+ var url = await _lsClient.GetBillingPortalUrl(sub.Id, cancellationToken);
+
+ return Ok(new { url });
+ }
+}
diff --git a/src/Features/Billing/BillingQueries.cs b/src/Features/Billing/BillingQueries.cs
new file mode 100755
index 0000000..129554e
--- /dev/null
+++ b/src/Features/Billing/BillingQueries.cs
@@ -0,0 +1,122 @@
+using Aptabase.Data;
+using Aptabase.Features.Authentication;
+using Aptabase.Features.Stats;
+using Dapper;
+
+namespace Aptabase.Features.Billing;
+
+public interface IBillingQueries
+{
+ Task GetTrialsDueSoon();
+ Task GetUserSubscription(UserIdentity user);
+ Task GetUserFreeTierOrTrial(UserIdentity user);
+ Task GetOwnedAppIds(UserIdentity user);
+ Task LockUsersWithExpiredTrials();
+ Task LockUser(string userId, string reason);
+ Task UnlockOveruseAccounts();
+ Task> GetBillingUsageByApp(int year, int month);
+ Task> GetUserQuotaForApps(string[] appIds);
+}
+
+public class BillingQueries : IBillingQueries
+{
+ private readonly IDbContext _db;
+ private readonly IQueryClient _queryClient;
+
+ public BillingQueries(IDbContext db, IQueryClient queryClient)
+ {
+ _db = db ?? throw new ArgumentNullException(nameof(db));
+ _queryClient = queryClient ?? throw new ArgumentNullException(nameof(queryClient));
+ }
+
+ public async Task GetUserSubscription(UserIdentity user)
+ {
+ return await _db.Connection.QueryFirstOrDefaultAsync(
+ @"SELECT * FROM subscriptions
+ WHERE owner_id = @userId
+ ORDER BY created_at DESC LIMIT 1",
+ new { userId = user.Id });
+ }
+
+ public async Task GetUserFreeTierOrTrial(UserIdentity user)
+ {
+ return await _db.Connection.QueryFirstAsync(
+ @"SELECT free_quota, free_trial_ends_at FROM users WHERE id = @userId",
+ new { userId = user.Id });
+ }
+
+ public async Task GetOwnedAppIds(UserIdentity user)
+ {
+ var releaseAppIds = await _db.Connection.QueryAsync(@"SELECT id FROM apps WHERE owner_id = @userId", new { userId = user.Id });
+ var debugAppIds = releaseAppIds.Select(id => $"{id}_DEBUG");
+ return releaseAppIds.Concat(debugAppIds).ToArray();
+ }
+
+ public async Task> GetBillingUsageByApp(int year, int month)
+ {
+ var period = $"{year}-{month.ToString().PadLeft(2, '0')}-01";
+ return await _queryClient.NamedQueryAsync("billing_usage_per_app__v1", new { period }, default);
+ }
+
+ public async Task> GetUserQuotaForApps(string[] appIds)
+ {
+ return await _db.Connection.QueryAsync(
+ @"SELECT u.id AS id,
+ u.email AS email,
+ u.name AS name,
+ ARRAY_AGG(a.id) as app_ids,
+ u.free_quota,
+ s.variant_id
+ FROM users u
+ LEFT JOIN subscriptions s
+ ON s.owner_id = u.id
+ AND s.status != 'expired'
+ INNER JOIN apps a
+ ON a.owner_id = u.id
+ AND a.id = ANY(@appIds)
+ WHERE u.lock_reason IS NULL
+ GROUP BY u.id, u.email, u.name, u.free_quota, s.variant_id", new { appIds });
+ }
+
+ public async Task GetTrialsDueSoon()
+ {
+ var start = DateTime.UtcNow.AddDays(5).Date;
+ var end = start.AddDays(1).Date;
+
+ var users = await _db.Connection.QueryAsync(
+ @"SELECT DISTINCT u.id, u.name, u.email
+ FROM users u
+ LEFT JOIN subscriptions s
+ ON s.owner_id = u.id
+ INNER JOIN apps a
+ ON a.owner_id = u.id
+ AND a.has_events = true
+ WHERE u.free_trial_ends_at BETWEEN @start AND @end
+ AND s.id IS NULL", new { start, end });
+ return users.ToArray();
+ }
+
+ public async Task LockUsersWithExpiredTrials()
+ {
+ var count = await _db.Connection.ExecuteAsync(
+ @"UPDATE users u
+ SET lock_reason = 'T'
+ WHERE u.free_quota IS NULL
+ AND u.free_trial_ends_at <= now()
+ AND u.lock_reason IS NULL
+ AND NOT EXISTS (SELECT 1 FROM subscriptions s WHERE s.owner_id = u.id)");
+ return count;
+ }
+
+ public async Task LockUser(string userId, string reason)
+ {
+ return await _db.Connection.ExecuteAsync(
+ @"UPDATE users SET lock_reason = @reason WHERE id = @userId",
+ new { userId, reason });
+ }
+
+ public async Task UnlockOveruseAccounts()
+ {
+ return await _db.Connection.ExecuteAsync(@"UPDATE users SET lock_reason = NULL WHERE lock_reason = 'O'");
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Billing/BillingUsage.cs b/src/Features/Billing/BillingUsage.cs
new file mode 100755
index 0000000..8fb1d32
--- /dev/null
+++ b/src/Features/Billing/BillingUsage.cs
@@ -0,0 +1,36 @@
+namespace Aptabase.Features.Billing;
+
+public class BillingUsage
+{
+ public long Count { get; set; }
+}
+
+public class BillingUsageByApp
+{
+ public string AppId { get; set; } = "";
+ public long Count { get; set; }
+}
+
+public class UserQuota
+{
+ public string Id { get; set; } = "";
+ public string Email { get; set; } = "";
+ public string Name { get; set; } = "";
+ public string[] AppIds { get; set; } = Array.Empty();
+ public long? FreeQuota { get; set; }
+ public long? VariantID { get; set; }
+
+ public long GetQuota()
+ {
+ if (VariantID is not null)
+ return SubscriptionPlan.GetByVariantId(VariantID.Value).MonthlyEvents;
+
+ return FreeQuota ?? SubscriptionPlan.FreeTrialMonthlyQuota;
+ }
+}
+
+public class BillingHistoricUsage
+{
+ public DateTime Date { get; set; }
+ public long Events { get; set; }
+}
\ No newline at end of file
diff --git a/src/Features/Billing/LemonSqueezy/LemonSqueezyClient.cs b/src/Features/Billing/LemonSqueezy/LemonSqueezyClient.cs
new file mode 100755
index 0000000..e8c0bf6
--- /dev/null
+++ b/src/Features/Billing/LemonSqueezy/LemonSqueezyClient.cs
@@ -0,0 +1,79 @@
+using System.Text.Json;
+using Aptabase.Features.Authentication;
+using Yoh.Text.Json.NamingPolicies;
+
+namespace Aptabase.Features.Billing.LemonSqueezy;
+
+public class LemonSqueezyClient
+{
+ private HttpClient _httpClient;
+ private EnvSettings _env;
+ private ILogger _logger;
+
+ public static readonly JsonSerializerOptions JsonSettings = new()
+ {
+ WriteIndented = false,
+ PropertyNamingPolicy = JsonNamingPolicies.SnakeCaseLower
+ };
+
+ public LemonSqueezyClient(IHttpClientFactory factory, EnvSettings env, ILogger logger)
+ {
+ _httpClient = factory.CreateClient("LemonSqueezy");
+ _env = env ?? throw new ArgumentNullException(nameof(env));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task CreateCheckout(UserIdentity user, CancellationToken cancellationToken)
+ {
+ var body = new {
+ data = new {
+ type = "checkouts",
+ attributes = new {
+ checkout_data = new {
+ email = user.Email,
+ custom = new {
+ user_id = user.Id,
+ region = _env.Region
+ }
+ },
+ product_options = new {
+ redirect_url = $"{_env.SelfBaseUrl}/billing",
+ },
+ checkout_options = new {
+ embed = false,
+ dark = false
+ }
+ },
+ relationships = new {
+ store = new {
+ data = new {
+ type = "stores",
+ id = "30508"
+ }
+ },
+ variant = new {
+ data = new {
+ type = "variants",
+ id = _env.IsProduction ? "103474" : "85183"
+ }
+ }
+ }
+ }
+ };
+
+ var response = await _httpClient.PostAsJsonAsync("/v1/checkouts", body, JsonSettings, cancellationToken);
+
+ await response.EnsureSuccessWithLog(_logger);
+ var result = await response.Content.ReadFromJsonAsync>>(JsonSettings);
+ return result?.Data.Attributes.Url ?? "";
+ }
+
+ public async Task GetBillingPortalUrl(long subscriptionId, CancellationToken cancellationToken)
+ {
+ var response = await _httpClient.GetAsync($"/v1/subscriptions/{subscriptionId}", cancellationToken);
+
+ await response.EnsureSuccessWithLog(_logger);
+ var result = await response.Content.ReadFromJsonAsync>>(JsonSettings);
+ return result?.Data.Attributes.Urls.CustomerPortal ?? "";
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Billing/LemonSqueezy/LemonSqueezyExtensions.cs b/src/Features/Billing/LemonSqueezy/LemonSqueezyExtensions.cs
new file mode 100755
index 0000000..0f92306
--- /dev/null
+++ b/src/Features/Billing/LemonSqueezy/LemonSqueezyExtensions.cs
@@ -0,0 +1,16 @@
+using System.Net.Http.Headers;
+
+namespace Aptabase.Features.Billing.LemonSqueezy;
+
+public static class LemonSqueezyExtensions
+{
+ public static void AddLemonSqueezy(this IServiceCollection services, EnvSettings env)
+ {
+ services.AddSingleton();
+ services.AddHttpClient("LemonSqueezy", client =>
+ {
+ client.BaseAddress = new Uri("https://api.lemonsqueezy.com");
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", env.LemonSqueezyApiKey);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Billing/LemonSqueezy/Models.cs b/src/Features/Billing/LemonSqueezy/Models.cs
new file mode 100755
index 0000000..8b03c61
--- /dev/null
+++ b/src/Features/Billing/LemonSqueezy/Models.cs
@@ -0,0 +1,38 @@
+namespace Aptabase.Features.Billing.LemonSqueezy;
+
+public class PagedList where T : new()
+{
+ public IEnumerable> Data { get; set; } = Enumerable.Empty>();
+}
+
+public class Resource where T : new()
+{
+ public string Id { get; set; } = "";
+ public string Type { get; set; } = "";
+ public T Attributes { get; set; } = new T();
+}
+
+public class GetResponse where T : new()
+{
+ public T Data { get; set; } = new T();
+}
+
+public class CheckoutAttributes
+{
+ public string Url { get; set; } = "";
+}
+
+public class SubscriptionAttributes
+{
+ public int ProductID { get; set; }
+ public int VariantID { get; set; }
+ public int CustomerID { get; set; }
+ public string Status { get; set; } = "";
+ public DateTime? EndsAt { get; set; }
+ public SubscriptionUrls Urls { get; set; } = new SubscriptionUrls();
+}
+
+public class SubscriptionUrls
+{
+ public string CustomerPortal { get; set; } = "";
+}
\ No newline at end of file
diff --git a/src/Features/Billing/LemonSqueezy/WebhookEvent.cs b/src/Features/Billing/LemonSqueezy/WebhookEvent.cs
new file mode 100755
index 0000000..110ad73
--- /dev/null
+++ b/src/Features/Billing/LemonSqueezy/WebhookEvent.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Nodes;
+
+namespace Aptabase.Features.Billing.LemonSqueezy;
+
+public class WebhookEvent
+{
+ public WebhookEventMeta Meta { get; set; } = new WebhookEventMeta();
+ public WebhookEventData Data { get; set; } = new WebhookEventData();
+}
+
+public class WebhookEventData
+{
+ public string Id { get; set; } = "";
+ public string Type { get; set; } = "";
+ public JsonObject Attributes { get; set; } = new JsonObject();
+}
+
+public class WebhookEventMeta
+{
+ public string EventName { get; set; } = "";
+ public Dictionary CustomData { get; set; } = new Dictionary();
+}
\ No newline at end of file
diff --git a/src/Features/Billing/LemonSqueezyWebhookController.cs b/src/Features/Billing/LemonSqueezyWebhookController.cs
new file mode 100755
index 0000000..c90dff4
--- /dev/null
+++ b/src/Features/Billing/LemonSqueezyWebhookController.cs
@@ -0,0 +1,163 @@
+using Aptabase.Data;
+using Aptabase.Features.Billing.LemonSqueezy;
+using Dapper;
+using Microsoft.AspNetCore.Mvc;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+
+namespace Aptabase.Features.Billing;
+
+[ApiController]
+[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
+public class LemonSqueezyWebhookController : Controller
+{
+ private readonly IDbContext _db;
+ private readonly LemonSqueezyClient _lsClient;
+ private readonly ILogger _logger;
+
+ private readonly string _region;
+ private readonly byte[] _signingSecret;
+
+ public LemonSqueezyWebhookController(EnvSettings env, IDbContext db, LemonSqueezyClient lsClient, ILogger logger)
+ {
+ _db = db ?? throw new ArgumentNullException(nameof(db));
+ _lsClient = lsClient ?? throw new ArgumentNullException(nameof(lsClient));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+
+ _region = env.Region;
+ _signingSecret = Encoding.UTF8.GetBytes(env.LemonSqueezySigningSecret);
+ }
+
+ [HttpPost("/webhook/lemonsqueezy")]
+ public async Task Post(CancellationToken cancellationToken)
+ {
+ var (isValid, body) = await ValidateSignatureAsync(HttpContext);
+
+ if (!isValid)
+ {
+ _logger.LogWarning("Invalid LemonSqueezy Signature");
+
+ return Unauthorized(new { message = "Invalid LemonSqueezy Signature" });
+ }
+
+ var ev = JsonSerializer.Deserialize(body, LemonSqueezyClient.JsonSettings);
+
+ if (ev == null)
+ {
+ _logger.LogWarning("Invalid JSON Body: {Body}", body);
+
+ return BadRequest(new { message = "Invalid JSON Body" });
+ }
+
+ if (ev.Meta.CustomData.TryGetValue("region", out var eventRegion) == false)
+ {
+ _logger.LogError("LemonSqueezy event is missing region");
+
+ return BadRequest(new { message = "Missing 'region' on meta.custom_data" });
+ }
+
+ if (eventRegion != _region)
+ {
+ return Ok(new { message = "Ignoring event from different region" });
+ }
+
+ var task = ev.Meta.EventName switch
+ {
+ "subscription_created" => HandleSubscriptionCreatedOrUpdated(ev, cancellationToken),
+ "subscription_updated" => HandleSubscriptionCreatedOrUpdated(ev, cancellationToken),
+ _ => HandleUnknownEvent(ev),
+ };
+
+ return await task;
+ }
+
+ private async Task HandleSubscriptionCreatedOrUpdated([FromBody] WebhookEvent ev, CancellationToken cancellationToken)
+ {
+ var body = JsonSerializer.Deserialize(ev.Data.Attributes, LemonSqueezyClient.JsonSettings);
+
+ if (body == null)
+ {
+ return BadRequest(new { message = "Event body is null" });
+ }
+
+ var subId = Convert.ToInt64(ev.Data.Id);
+ var ownerId = ev.Meta.CustomData["user_id"];
+
+ if (string.IsNullOrEmpty(ownerId))
+ {
+ _logger.LogError("LemonSqueezy event is missing user_id");
+
+ return BadRequest(new { message = "Missing 'user_id' on meta.custom_data" });
+ }
+
+ await _db.Connection.ExecuteAsync(
+ @"UPDATE users SET lock_reason = null WHERE id = @ownerId",
+ new { ownerId }
+ );
+
+ if (body.Status == "expired")
+ {
+ await _db.Connection.ExecuteAsync(
+ @"DELETE FROM subscriptions WHERE id = @subId",
+ new
+ {
+ subId,
+ });
+ }
+ else
+ {
+ await _db.Connection.ExecuteAsync(
+ @"INSERT INTO subscriptions
+ (id, owner_id, customer_id, product_id, variant_id, status, ends_at)
+ VALUES
+ (@subId, @ownerId, @customerId, @productId, @variantId, @status, @endsAt)
+ ON CONFLICT (id)
+ DO UPDATE SET product_id = @productId,
+ variant_id = @variantId,
+ status = @status,
+ ends_at = @endsAt,
+ modified_at = now()",
+ new
+ {
+ subId,
+ ownerId,
+ customerId = body.CustomerID,
+ productId = body.ProductID,
+ variantId = body.VariantID,
+ status = body.Status,
+ endsAt = body.EndsAt,
+ });
+ }
+
+ return Ok(new {});
+ }
+
+ private async Task<(bool, string)> ValidateSignatureAsync(HttpContext http)
+ {
+ using var reader = new StreamReader(Request.Body);
+ string body = await reader.ReadToEndAsync();
+
+ using var hmac = new HMACSHA256(_signingSecret);
+ var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(body));
+ var digest = BitConverter.ToString(hash).Replace("-", string.Empty).ToLower();
+ var digestBytes = Encoding.UTF8.GetBytes(digest);
+
+ var signature = http.Request.Headers["X-Signature"].ToString() ?? string.Empty;
+ var signatureBytes = Encoding.UTF8.GetBytes(signature);
+
+ if (!CryptographicOperations.FixedTimeEquals(digestBytes, signatureBytes))
+ {
+ return (false, string.Empty);
+ }
+
+ return (true, body);
+ }
+
+ private Task HandleUnknownEvent(WebhookEvent ev)
+ {
+ _logger.LogError("Unknown LemonSqueezy event: {EventName}", ev.Meta.EventName);
+
+ return Task.FromResult(BadRequest(new { message = "Unknown event" }));
+ }
+}
diff --git a/src/Features/Billing/OveruseNotificationCronJob.cs b/src/Features/Billing/OveruseNotificationCronJob.cs
new file mode 100755
index 0000000..cad53b7
--- /dev/null
+++ b/src/Features/Billing/OveruseNotificationCronJob.cs
@@ -0,0 +1,100 @@
+using Aptabase.Features.Notification;
+using Features.Cache;
+using Sgbj.Cron;
+
+namespace Aptabase.Features.Billing;
+
+public class OveruseNotificationCronJob : BackgroundService
+{
+ private readonly IBillingQueries _billingQueries;
+ private readonly IEmailClient _emailClient;
+ private readonly EnvSettings _env;
+ private readonly ICache _cache;
+ private readonly ILogger _logger;
+
+ public OveruseNotificationCronJob(IBillingQueries billingQueries, IEmailClient emailClient, EnvSettings env, ICache cache, ILogger logger)
+ {
+ _billingQueries = billingQueries ?? throw new ArgumentNullException(nameof(billingQueries));
+ _emailClient = emailClient ?? throw new ArgumentNullException(nameof(emailClient));
+ _env = env ?? throw new ArgumentNullException(nameof(env));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _cache = cache ?? throw new ArgumentNullException(nameof(cache));
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ _logger.LogInformation("OveruseNotificationCronJob is starting.");
+
+ using var timer = new CronTimer("10 * * * *", TimeZoneInfo.Utc);
+
+ while (await timer.WaitForNextTickAsync(cancellationToken))
+ {
+ _logger.LogInformation("Looking for accounts with events overuse.");
+ var usagePerApp = await _billingQueries.GetBillingUsageByApp(DateTime.UtcNow.Year, DateTime.UtcNow.Month);
+ var users = await _billingQueries.GetUserQuotaForApps(usagePerApp.Select(x => x.AppId).ToArray());
+ foreach (var user in users)
+ {
+ try
+ {
+ var quota = user.GetQuota();
+ var usage = usagePerApp.Where(x => user.AppIds.Contains(x.AppId)).Sum(x => x.Count);
+ var perc = usage * 1.0 / quota;
+ _logger.LogInformation("User {UserId} has used {Usage} ({Perc:P}) out of {Quota} events.", user.Id, usage, perc, quota);
+
+ var (subject, templateName) = GetSubjectTemplateName(perc);
+ if (string.IsNullOrEmpty(templateName) || string.IsNullOrEmpty(subject))
+ continue; // No need to send notification
+
+ var cacheKey = $"OveruseNotification.{user.Id}.{templateName}.{DateTime.UtcNow.Year}-{DateTime.UtcNow.Month.ToString().PadLeft(2, '0')}";
+ if (await _cache.Exists(cacheKey))
+ continue; // Already sent notification
+
+ await _emailClient.SendEmailAsync(user.Email, subject, templateName, new()
+ {
+ { "name", user.Name.Split(" ").ElementAtOrDefault(0) ?? user.Name },
+ { "quota", quota.ToString("N0") },
+ { "url", _env.SelfBaseUrl },
+ }, cancellationToken);
+
+ if (usage >= quota)
+ {
+ _logger.LogInformation("User {UserId} has reached the limit, pausing ingestion.", user.Id);
+ await _billingQueries.LockUser(user.Id, "O");
+ }
+
+ await _cache.Set(cacheKey, DateTime.UtcNow.ToString("o"), TimeSpan.FromDays(10));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "OveruseNotificationCronJob failed to process usage for user {UserId}.", user.Id);
+ }
+ }
+ _logger.LogInformation("Finished processing overuse notifications.");
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.LogInformation("OveruseNotificationCronJob stopped.");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "OveruseNotificationCronJob crashed.");
+ }
+ }
+ }
+
+ private static (string?, string?) GetSubjectTemplateName(double perc)
+ {
+ if (perc >= 1)
+ return ("ACTION REQUIRED: Event ingestion paused", "UsageLevel100");
+ if (perc >= 0.9)
+ return ("WARNING: You have used 90% of your monthly limit, incoming events will be dropped soon", "UsageLevel90");
+ if (perc >= 0.8)
+ return ("You have used 80% of your monthly limit, incoming events will be dropped soon", "UsageLevel80");
+ return (null, null);
+ }
+}
diff --git a/src/Features/Billing/ResetOveruseCronJob.cs b/src/Features/Billing/ResetOveruseCronJob.cs
new file mode 100755
index 0000000..21b7c95
--- /dev/null
+++ b/src/Features/Billing/ResetOveruseCronJob.cs
@@ -0,0 +1,43 @@
+using Sgbj.Cron;
+
+namespace Aptabase.Features.Billing;
+
+public class ResetOveruseCronJob : BackgroundService
+{
+ private readonly IBillingQueries _billingQueries;
+ private readonly ILogger _logger;
+
+ public ResetOveruseCronJob(IBillingQueries billingQueries, ILogger logger)
+ {
+ _billingQueries = billingQueries ?? throw new ArgumentNullException(nameof(billingQueries));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ _logger.LogInformation("ResetOveruseCronJob is starting.");
+
+ using var timer = new CronTimer("5 0 1 * *", TimeZoneInfo.Utc);
+
+ while (await timer.WaitForNextTickAsync(cancellationToken))
+ {
+ _logger.LogInformation("Unlocking accounts with overuse.");
+ var count = await _billingQueries.UnlockOveruseAccounts();
+ _logger.LogInformation("Unlocked {count} accounts with overuse status.", count);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.LogInformation("ResetOveruseCronJob stopped.");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "ResetOveruseCronJob crashed.");
+ }
+ }
+ }
+}
diff --git a/src/Features/Billing/Subscription.cs b/src/Features/Billing/Subscription.cs
new file mode 100755
index 0000000..9e55d02
--- /dev/null
+++ b/src/Features/Billing/Subscription.cs
@@ -0,0 +1,87 @@
+
+namespace Aptabase.Features.Billing;
+
+public class Subscription
+{
+ public long Id { get; set; }
+ public string OwnerId { get; set; } = "";
+ public long CustomerId { get; set; }
+ public long ProductId { get; set; }
+ public long VariantId { get; set; }
+ public string Status { get; set; } = "";
+ public DateTime? EndsAt { get; set; }
+ public DateTime CreatedAt { get; set; }
+ public DateTime ModifiedAt { get; set; }
+}
+
+public class FreeSubscription
+{
+ public long? FreeQuota { get; set; }
+ public DateTime? FreeTrialEndsAt { get; set; }
+}
+
+public class SubscriptionPlan
+{
+ public const int FreeTrialMonthlyQuota = 1_000_000;
+
+ public string Name { get; }
+ public int MonthlyPrice { get; }
+ public int MonthlyEvents { get; }
+ public long VariantId { get; }
+ public DateTime? FreeTrialEndsAt { get; }
+
+ public SubscriptionPlan(string name, int monthlyEvents, int monthlyPrice, long variantId, DateTime? freeTrialEndsAt)
+ {
+ Name = name;
+ MonthlyEvents = monthlyEvents;
+ MonthlyPrice = monthlyPrice;
+ VariantId = variantId;
+ FreeTrialEndsAt = freeTrialEndsAt;
+ }
+
+ public static SubscriptionPlan GetFreeVariant(FreeSubscription sub)
+ {
+ if (sub.FreeQuota.HasValue)
+ return new SubscriptionPlan("Free Plan", (int)sub.FreeQuota.Value, 0, 0, null);
+
+ if (sub.FreeTrialEndsAt.HasValue)
+ return new SubscriptionPlan("Free Trial", FreeTrialMonthlyQuota, 0, 0, sub.FreeTrialEndsAt);
+
+ throw new InvalidOperationException("No free subscription found");
+ }
+
+ public static SubscriptionPlan GetByVariantId(long variantId)
+ {
+ return ProductionPlans.FirstOrDefault(plan => plan.VariantId == variantId)
+ ?? DevelopmentPlans.FirstOrDefault(plan => plan.VariantId == variantId)
+ ?? throw new InvalidOperationException($"Subscription Variant not found for ID {variantId}");
+ }
+
+ private static readonly SubscriptionPlan[] DevelopmentPlans =
+ [
+ new SubscriptionPlan("200k Plan", 200_000, 10, 85183, null),
+ new SubscriptionPlan("1M Plan", 1_000_000, 20, 85184, null),
+ new SubscriptionPlan("2M Plan", 2_000_000, 40, 85185, null),
+ new SubscriptionPlan("5M Plan", 5_000_000, 75, 85187, null),
+ new SubscriptionPlan("10M Plan", 10_000_000, 140, 85188, null),
+ new SubscriptionPlan("20M Plan", 20_000_000, 240, 85190, null),
+ new SubscriptionPlan("30M Plan", 30_000_000, 300, 85192, null),
+ new SubscriptionPlan("50M Plan", 50_000_000, 450, 85194, null),
+ ];
+
+ private static readonly SubscriptionPlan[] ProductionPlans =
+ [
+ new SubscriptionPlan("200k Plan", 200_000, 10, 103474, null),
+ new SubscriptionPlan("1M Plan", 1_000_000, 20, 103475, null),
+ new SubscriptionPlan("2M Plan", 2_000_000, 40, 103476, null),
+ new SubscriptionPlan("5M Plan", 5_000_000, 75, 103477, null),
+ new SubscriptionPlan("10M Plan", 10_000_000, 140, 103478, null),
+ new SubscriptionPlan("20M Plan", 20_000_000, 240, 103479, null),
+ new SubscriptionPlan("30M Plan", 30_000_000, 300, 103480, null),
+ new SubscriptionPlan("50M Plan", 50_000_000, 450, 103481, null),
+ new SubscriptionPlan("100M Plan", 100_000_000, 750, 614418, null),
+ new SubscriptionPlan("200M Plan", 200_000_000, 1200, 614417, null),
+ new SubscriptionPlan("500M Plan", 500_000_000, 1500, 636430, null),
+
+ ];
+}
\ No newline at end of file
diff --git a/src/Features/Billing/TrialExpiredCronJob.cs b/src/Features/Billing/TrialExpiredCronJob.cs
new file mode 100755
index 0000000..4aa9c0e
--- /dev/null
+++ b/src/Features/Billing/TrialExpiredCronJob.cs
@@ -0,0 +1,43 @@
+using Sgbj.Cron;
+
+namespace Aptabase.Features.Billing;
+
+public class TrialExpiredCronJob : BackgroundService
+{
+ private readonly IBillingQueries _billingQueries;
+ private readonly ILogger _logger;
+
+ public TrialExpiredCronJob(IBillingQueries billingQueries, ILogger logger)
+ {
+ _billingQueries = billingQueries ?? throw new ArgumentNullException(nameof(billingQueries));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ _logger.LogInformation("TrialExpiredCronJob is starting.");
+
+ using var timer = new CronTimer("0 0 * * *", TimeZoneInfo.Utc);
+
+ while (await timer.WaitForNextTickAsync(cancellationToken))
+ {
+ _logger.LogInformation("Searching for users with expired trial.");
+ var count = await _billingQueries.LockUsersWithExpiredTrials();
+ _logger.LogInformation("Locked {count} users with expired trial.", count);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.LogInformation("TrialExpiredCronJob stopped.");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "TrialExpiredCronJob crashed.");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Billing/TrialNotificationCronJob.cs b/src/Features/Billing/TrialNotificationCronJob.cs
new file mode 100755
index 0000000..ba948d6
--- /dev/null
+++ b/src/Features/Billing/TrialNotificationCronJob.cs
@@ -0,0 +1,58 @@
+using Aptabase.Features.Notification;
+using Sgbj.Cron;
+
+namespace Aptabase.Features.Billing;
+
+public class TrialNotificationCronJob : BackgroundService
+{
+ private readonly IBillingQueries _billingQueries;
+ private readonly IEmailClient _emailClient;
+ private readonly EnvSettings _env;
+ private readonly ILogger _logger;
+
+ public TrialNotificationCronJob(IBillingQueries billingQueries, IEmailClient emailClient, EnvSettings env, ILogger logger)
+ {
+ _billingQueries = billingQueries ?? throw new ArgumentNullException(nameof(billingQueries));
+ _emailClient = emailClient ?? throw new ArgumentNullException(nameof(emailClient));
+ _env = env ?? throw new ArgumentNullException(nameof(env));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ _logger.LogInformation("TrialNotificationCronJob is starting.");
+
+ using var timer = new CronTimer("0 0 * * *", TimeZoneInfo.Utc);
+
+ while (await timer.WaitForNextTickAsync(cancellationToken))
+ {
+ _logger.LogInformation("Searching for users to notify about trial expiration.");
+ var users = await _billingQueries.GetTrialsDueSoon();
+ foreach (var user in users)
+ {
+ _logger.LogInformation("Sending trial notification to {name} ({user})", user.Name, user.Email);
+ await _emailClient.SendEmailAsync(user.Email, "Your Trial ends in 5 days", "TrialEndsSoon", new()
+ {
+ { "name", user.Name },
+ { "url", _env.SelfBaseUrl },
+ }, cancellationToken);
+ }
+
+ _logger.LogInformation("Sent trial notifications to {count} users", users.Length);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.LogInformation("TrialNotificationCronJob stopped.");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "TrialNotificationCronJob crashed.");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Blob/DatabaseBlobService.cs b/src/Features/Blob/DatabaseBlobService.cs
new file mode 100755
index 0000000..ec84b71
--- /dev/null
+++ b/src/Features/Blob/DatabaseBlobService.cs
@@ -0,0 +1,36 @@
+using Aptabase.Data;
+using ClickHouse.Client.Utility;
+using Dapper;
+
+namespace Aptabase.Features.Blob;
+
+public class DatabaseBlobService : IBlobService
+{
+ private readonly IDbContext _db;
+
+ public DatabaseBlobService(IDbContext db)
+ {
+ _db = db ?? throw new ArgumentNullException(nameof(db));
+ }
+
+ public async Task UploadAsync(string prefix, byte[] content, string contentType, CancellationToken cancellationToken)
+ {
+ var path = $"{prefix}/{Guid.NewGuid().ToString()}.png";
+
+ var cmd = new CommandDefinition("INSERT INTO blobs (path, content, content_type) VALUES (@path, @content, @contentType)", new {
+ path,
+ content = content,
+ contentType = "image/png"
+ }, cancellationToken: cancellationToken);
+
+ await _db.Connection.ExecuteScalarAsync(cmd);
+
+ return path;
+ }
+
+ public async Task DownloadAsync(string path, CancellationToken cancellationToken)
+ {
+ var cmd = new CommandDefinition("SELECT path, content, content_type FROM blobs WHERE path = @path", new { path }, cancellationToken: cancellationToken);
+ return await _db.Connection.QuerySingleOrDefaultAsync(cmd);
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Blob/IBlobService.cs b/src/Features/Blob/IBlobService.cs
new file mode 100755
index 0000000..4abab21
--- /dev/null
+++ b/src/Features/Blob/IBlobService.cs
@@ -0,0 +1,14 @@
+namespace Aptabase.Features.Blob;
+
+public class Blob
+{
+ public string Path { get; set; } = "";
+ public byte[] Content { get; set; } = new byte[0];
+ public string ContentType { get; set; } = "";
+}
+
+public interface IBlobService
+{
+ Task UploadAsync(string prefix, byte[] content, string contentType, CancellationToken cancellationToken);
+ Task DownloadAsync(string path, CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/src/Features/Blob/UploadsController.cs b/src/Features/Blob/UploadsController.cs
new file mode 100755
index 0000000..2dd8ea2
--- /dev/null
+++ b/src/Features/Blob/UploadsController.cs
@@ -0,0 +1,33 @@
+using Aptabase.Features.Blob;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Aptabase.Features.Blob;
+
+public class UploadedFile
+{
+ public byte[] Content { get; set; } = new byte[0];
+ public string ContentType { get; set; } = "";
+}
+
+[ApiController, AllowAnonymous]
+public class UploadsController : Controller
+{
+ private readonly IBlobService _blobService;
+
+ public UploadsController(IBlobService blobService)
+ {
+ _blobService = blobService ?? throw new ArgumentNullException(nameof(blobService));
+ }
+
+ [HttpGet("/uploads/{**path}")]
+ [ResponseCache(Duration = 365 * 24 * 60 * 60)] // Cache for 1 year because uploaded files are immutable
+ public async Task ReadFile(string path, CancellationToken cancellationToken)
+ {
+ var file = await _blobService.DownloadAsync(path, cancellationToken);
+ if (file == null)
+ return NotFound();
+
+ return File(file.Content, file.ContentType);
+ }
+}
diff --git a/src/Features/Cache/CacheService.cs b/src/Features/Cache/CacheService.cs
new file mode 100755
index 0000000..ec61a35
--- /dev/null
+++ b/src/Features/Cache/CacheService.cs
@@ -0,0 +1,43 @@
+using Aptabase.Data;
+using Dapper;
+
+namespace Features.Cache;
+
+public interface ICache
+{
+ Task Get(string key);
+ Task Set(string key, string value, TimeSpan ttl);
+ Task Exists(string key);
+}
+
+public class DatabaseCache : ICache
+{
+ private readonly IDbContext _db;
+
+ public DatabaseCache(IDbContext db)
+ {
+ _db = db ?? throw new ArgumentNullException(nameof(db));
+ }
+
+ public async Task Get(string key)
+ {
+ return await _db.Connection.ExecuteScalarAsync(
+ "SELECT value FROM cache WHERE key = @key AND expires_at > now()",
+ new { key });
+ }
+
+ public async Task Set(string key, string value, TimeSpan ttl)
+ {
+ var expiresAt = DateTime.UtcNow.Add(ttl);
+ await _db.Connection.ExecuteAsync(
+ "INSERT INTO cache (key, value, expires_at) VALUES (@key, @value, @expiresAt) " +
+ "ON CONFLICT (key) DO UPDATE SET value = @value, expires_at = @expiresAt",
+ new { key, value, expiresAt });
+ }
+
+ public async Task Exists(string key)
+ {
+ var value = await Get(key);
+ return !string.IsNullOrEmpty(value);
+ }
+}
\ No newline at end of file
diff --git a/src/Features/EnvSettings.cs b/src/Features/EnvSettings.cs
new file mode 100755
index 0000000..118fd59
--- /dev/null
+++ b/src/Features/EnvSettings.cs
@@ -0,0 +1,153 @@
+using System.Text;
+
+namespace Aptabase.Features;
+
+public class EnvSettings
+{
+ // The base URL of the application, used for generating links
+ // E.g: https://analytics.yourdomain.com
+ // Variable Name: BASE_URL
+ public string SelfBaseUrl { get; private set; } = "";
+
+ // The full connection string to postgres using .NET format
+ // E.g: Server=localhost;Port=5444;User Id=aptabase;Password=aptabase_pw;Database=aptabase
+ // Variable Name: DATABASE_URL
+ public string ConnectionString { get; private set; } = "";
+
+ // The full connection string to ClickHouse using .NET format
+ // E.g: Host=my.clickhouse;Protocol=https;Port=12345;Username=user
+ // Variable Name: CLICKHOUSE_URL
+ public string ClickHouseConnectionString { get; private set; } = "";
+
+ // The base URI of the Tinybird API
+ // E.g: https://api.tinybird.co
+ // Variable Name: TINYBIRD_BASE_URL
+ public string TinybirdBaseUrl { get; private set; } = "";
+
+ // The token for Tinybird API, must have write and read access
+ // E.g: p.eyJ1Ijo...
+ // Variable Name: TINYBIRD_TOKEN
+ public string TinybirdToken { get; private set; } = "";
+
+ // A random secret key used for signing auth tokens
+ // E.g: GMvqFuPEiRZt6RtaB5OT
+ // Variable Name: AUTH_SECRET
+ public byte[] AuthSecret { get; private set; } = [];
+
+ public string? MailCatcherConnectionString { get; private set; }
+
+ // The host of the SMTP server
+ // E.g: smtp.someprovider.com
+ // Variable Name: SMTP_HOST
+ public string SmtpHost { get; private set; } = "";
+
+ // The port used for sending emails via SMTP
+ // E.g: 587
+ // Variable Name: SMTP_PORT
+ public int SmtpPort { get; private set; } = 0;
+
+ // The username for the SMTP server
+ // Variable Name: SMTP_USERNAME
+ public string SmtpUsername { get; private set; } = "";
+
+ // The password for the SMTP server
+ // Variable Name: SMTP_PASSWORD
+ public string SmtpPassword { get; private set; } = "";
+
+ // Address to send the email from
+ // E.g.: notification@yourdomain.com
+ // Variable Name: SMTP_FROM_ADDRESS
+ public string SmtpFromAddress { get; private set; } = "";
+
+ // The GitHub Client ID for OAuth
+ // Variable Name: OAUTH_GITHUB_CLIENT_ID
+ public string OAuthGitHubClientId { get; private set; } = "";
+
+ // The GitHub Client Secret for OAuth
+ // Variable Name: OAUTH_GITHUB_CLIENT_SECRET
+ public string OAuthGitHubClientSecret { get; private set; } = "";
+
+ // The Google Client ID for OAuth
+ // Variable Name: OAUTH_GOOGLE_CLIENT_ID
+ public string OAuthGoogleClientId { get; private set; } = "";
+
+ // The Google Client Secret for OAuth
+ // Variable Name: OAUTH_GOOGLE_CLIENT_SECRET
+ public string OAuthGoogleClientSecret { get; private set; } = "";
+
+ // The following properties are derived from the other settings
+ public bool IsManagedCloud => Region == "EU" || Region == "US";
+ public bool IsBillingEnabled => IsManagedCloud || IsDevelopment;
+ public bool IsProduction => !IsDevelopment;
+ public bool IsDevelopment { get; private set; }
+ public string Region { get; private set; } = "";
+ public string LemonSqueezyApiKey { get; private set; } = "";
+ public string LemonSqueezySigningSecret { get; private set; } = "";
+ public string EtcDirectoryPath { get; private set; } = "";
+
+ public static EnvSettings Load()
+ {
+ var isDevelopment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development";
+ var region = isDevelopment ? "DEV" : Get("REGION").ToUpper();
+ if (string.IsNullOrEmpty(region))
+ region = "ZV"; // Self Hosted
+
+ return new EnvSettings
+ {
+ IsDevelopment = isDevelopment,
+ Region = region,
+ SelfBaseUrl = MustGet("BASE_URL"),
+ ConnectionString = GetOrNull("ConnectionStrings__postgresdb") ?? MustGet("DATABASE_URL"),
+ ClickHouseConnectionString = GetOrNull("ConnectionStrings__clickhousedb") ?? Get("CLICKHOUSE_URL"),
+ TinybirdBaseUrl = Get("TINYBIRD_BASE_URL"),
+ TinybirdToken = Get("TINYBIRD_TOKEN"),
+ AuthSecret = Encoding.ASCII.GetBytes(MustGet("AUTH_SECRET")),
+ LemonSqueezyApiKey = Get("LEMONSQUEEZY_API_KEY"),
+ LemonSqueezySigningSecret = Get("LEMONSQUEEZY_SIGNING_SECRET"),
+
+ SmtpHost = Get("SMTP_HOST"),
+ SmtpPort = GetInt("SMTP_PORT"),
+ SmtpUsername = Get("SMTP_USERNAME"),
+ SmtpPassword = Get("SMTP_PASSWORD"),
+ SmtpFromAddress = Get("SMTP_FROM_ADDRESS"),
+ MailCatcherConnectionString = GetOrNull("ConnectionStrings__mailcatcher"),
+
+ OAuthGitHubClientId = Get("OAUTH_GITHUB_CLIENT_ID"),
+ OAuthGitHubClientSecret = Get("OAUTH_GITHUB_CLIENT_SECRET"),
+ OAuthGoogleClientId = Get("OAUTH_GOOGLE_CLIENT_ID"),
+ OAuthGoogleClientSecret = Get("OAUTH_GOOGLE_CLIENT_SECRET"),
+
+ // On the container, the etc directory is mounted at ./etc
+ // But during development, it's at ../etc
+ EtcDirectoryPath = Directory.Exists("./etc") ? "./etc" : "../etc"
+ };
+ }
+
+ private EnvSettings()
+ {
+
+ }
+
+ private static string? GetOrNull(string name)
+ {
+ return Environment.GetEnvironmentVariable(name);
+ }
+
+ private static string Get(string name)
+ {
+ return Environment.GetEnvironmentVariable(name) ?? "";
+ }
+
+ private static int GetInt(string name)
+ {
+ var value = Environment.GetEnvironmentVariable(name) ?? "";
+ if (int.TryParse(value, out var result))
+ return result;
+ return 0;
+ }
+
+ private static string MustGet(string name)
+ {
+ return Environment.GetEnvironmentVariable(name) ?? throw new Exception($"Missing {name} environment variable");
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Export/ExportController.cs b/src/Features/Export/ExportController.cs
new file mode 100755
index 0000000..d27844a
--- /dev/null
+++ b/src/Features/Export/ExportController.cs
@@ -0,0 +1,436 @@
+using Aptabase.Features.Authentication;
+using Aptabase.Features.Stats;
+using Microsoft.AspNetCore.Mvc;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+
+namespace Aptabase.Features.Export;
+
+public class DownloadRequest
+{
+ public string BuildMode { get; set; } = "";
+ public string AppId { get; set; } = "";
+ public string AppName { get; set; } = "";
+ public string Format { get; set; } = "";
+ public DateTime? StartDate { get; set; }
+ public DateTime? EndDate { get; set; }
+}
+
+public class MonthlyUsage
+{
+ public int Year { get; set; }
+ public int Month { get; set; }
+ public long Events { get; set; }
+}
+
+[ApiController, IsAuthenticated, HasReadAccessToApp]
+[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
+public partial class ExportController(IQueryClient queryClient, ILogger logger) : Controller
+{
+ private readonly IQueryClient _queryClient = queryClient ?? throw new ArgumentNullException(nameof(queryClient));
+ private readonly ILogger _logger = logger;
+
+ [HttpGet("/api/_export/usage")]
+ public async Task MonthlyUsage([FromQuery] string buildMode, [FromQuery] string appId, CancellationToken cancellationToken)
+ {
+ var rows = await _queryClient.NamedQueryAsync("monthly_usage__v1", new {
+ app_id = GetAppId(buildMode, appId)
+ }, cancellationToken);
+
+ if (buildMode.Equals("debug", StringComparison.OrdinalIgnoreCase))
+ {
+ return Ok(rows.Take(5));
+ }
+
+ return Ok(rows);
+ }
+
+ [HttpGet("/api/_export/download")]
+ public async Task Download([FromQuery] DownloadRequest body)
+ {
+ if (!body.StartDate.HasValue || !body.EndDate.HasValue)
+ {
+ return BadRequest();
+ }
+
+ var startDate = body.StartDate.Value;
+ var endDate = body.EndDate.Value;
+
+ var (formatName, contentType, fileExtension) = GetFormat(body.Format);
+ var appName = UnsafeCharacters().Replace(body.AppName, "").ToLower();
+ var fileName = $"{appName}-{body.BuildMode.ToLower()}-{startDate:yyyy-MM-dd}.{fileExtension}";
+
+ var countQuery = $@"SELECT COUNT(*) as event_count
+ FROM events
+ WHERE app_id = '{GetAppId(body.BuildMode, body.AppId)}'
+ AND timestamp BETWEEN '{startDate:yyyy-MM-dd HH:mm:ss}' AND '{endDate:yyyy-MM-dd HH:mm:ss}'
+ FORMAT JSON";
+
+ using var stream = await _queryClient.StreamResponseAsync(countQuery, HttpContext.RequestAborted);
+ long eventCount = 0;
+ using var reader = new StreamReader(stream);
+ var jsonString = await reader.ReadToEndAsync();
+ using var doc = JsonDocument.Parse(jsonString);
+ var root = doc.RootElement;
+
+ if (root.TryGetProperty("data", out JsonElement data) &&
+ data.GetArrayLength() > 0 &&
+ data[0].TryGetProperty("event_count", out JsonElement countElement))
+ {
+ switch (countElement.ValueKind)
+ {
+ case JsonValueKind.Number:
+ _ = countElement.TryGetInt64(out eventCount);
+ break;
+ case JsonValueKind.String:
+ var countString = countElement.GetString();
+ _ = long.TryParse(countString, out eventCount);
+ break;
+ default:
+ throw new InvalidOperationException($"Unexpected event_count type: {countElement.ValueKind}");
+ }
+ }
+
+ if (formatName == "Parquet")
+ {
+ var query = $@"SELECT timestamp, user_id, session_id,
+ event_name,
+ replace(replace(string_props, '\u0022', '\''), '\u0027', '\'') as string_props,
+ numeric_props,
+ os_name, os_version,
+ locale, app_version, app_build_number,
+ engine_name, engine_version,
+ country_code, {COUNTRY_NAME_COLUMN}, region_name
+ FROM events
+ WHERE app_id = '{GetAppId(body.BuildMode, body.AppId)}'
+ AND timestamp BETWEEN '{startDate:yyyy-MM-dd HH:mm:ss}' AND '{endDate:yyyy-MM-dd HH:mm:ss}'
+ ORDER BY timestamp DESC
+ FORMAT {formatName}";
+
+ return File(await _queryClient.StreamResponseAsync(query, HttpContext.RequestAborted), contentType, fileName);
+ }
+
+ return new StreamingFileResult(async (stream, httpContext, cancellationToken) =>
+ {
+ httpContext.Response.StatusCode = StatusCodes.Status200OK;
+
+ // todo use s3 buckets
+ try
+ {
+ const int pageSize = 100000;
+ long processedRecords = 0;
+
+ while (processedRecords < eventCount && !cancellationToken.IsCancellationRequested)
+ {
+ var query = $@"
+ SELECT timestamp, user_id, session_id,
+ event_name,
+ replace(replace(string_props, '\u0022', '\''), '\u0027', '\'') as string_props,
+ numeric_props,
+ os_name, os_version,
+ locale, app_version, app_build_number,
+ engine_name, engine_version,
+ country_code, {COUNTRY_NAME_COLUMN}, region_name
+ FROM events
+ WHERE app_id = '{GetAppId(body.BuildMode, body.AppId)}'
+ AND timestamp BETWEEN '{startDate:yyyy-MM-dd HH:mm:ss}' AND '{endDate:yyyy-MM-dd HH:mm:ss}'
+ ORDER BY timestamp DESC
+ LIMIT {pageSize}
+ OFFSET {processedRecords}
+ FORMAT {formatName}";
+
+ using var pageStream = await _queryClient.StreamResponseAsync(query, cancellationToken);
+ await pageStream.CopyToAsync(stream, cancellationToken);
+
+ processedRecords += pageSize;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error occurred while streaming download");
+
+ if (!httpContext.Response.HasStarted)
+ {
+ httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
+ await httpContext.Response.WriteAsync("An error occurred while processing your request.", cancellationToken);
+ }
+ }
+
+ }, contentType, fileName);
+ }
+
+ private static string GetAppId(string buildMode, string appId)
+ {
+ return buildMode.ToLower() switch
+ {
+ "debug" => $"{appId}_DEBUG",
+ _ => appId,
+ };
+ }
+
+ private static (string, string, string) GetFormat(string format)
+ {
+ return format.ToLower() switch
+ {
+ "parquet" => ("Parquet", "application/octet-stream", "parquet"),
+ _ => ("CSVWithNames", "text/csv", "csv"),
+ };
+ }
+
+ [GeneratedRegex("[^a-zA-Z0-9]")]
+ private static partial Regex UnsafeCharacters();
+
+ private static readonly string COUNTRY_NAME_COLUMN = @"
+ CASE country_code
+ WHEN 'AF' THEN 'Afghanistan'
+ WHEN 'AX' THEN 'Aland Islands'
+ WHEN 'AL' THEN 'Albania'
+ WHEN 'DZ' THEN 'Algeria'
+ WHEN 'AS' THEN 'American Samoa'
+ WHEN 'AD' THEN 'Andorra'
+ WHEN 'AO' THEN 'Angola'
+ WHEN 'AI' THEN 'Anguilla'
+ WHEN 'AQ' THEN 'Antarctica'
+ WHEN 'AG' THEN 'Antigua And Barbuda'
+ WHEN 'AR' THEN 'Argentina'
+ WHEN 'AM' THEN 'Armenia'
+ WHEN 'AW' THEN 'Aruba'
+ WHEN 'AU' THEN 'Australia'
+ WHEN 'AT' THEN 'Austria'
+ WHEN 'AZ' THEN 'Azerbaijan'
+ WHEN 'BS' THEN 'Bahamas'
+ WHEN 'BH' THEN 'Bahrain'
+ WHEN 'BD' THEN 'Bangladesh'
+ WHEN 'BB' THEN 'Barbados'
+ WHEN 'BY' THEN 'Belarus'
+ WHEN 'BE' THEN 'Belgium'
+ WHEN 'BZ' THEN 'Belize'
+ WHEN 'BJ' THEN 'Benin'
+ WHEN 'BM' THEN 'Bermuda'
+ WHEN 'BT' THEN 'Bhutan'
+ WHEN 'BO' THEN 'Bolivia'
+ WHEN 'BA' THEN 'Bosnia And Herzegovina'
+ WHEN 'BW' THEN 'Botswana'
+ WHEN 'BV' THEN 'Bouvet Island'
+ WHEN 'BR' THEN 'Brazil'
+ WHEN 'IO' THEN 'British Indian Ocean Territory'
+ WHEN 'BN' THEN 'Brunei Darussalam'
+ WHEN 'BQ' THEN 'Bonaire, Saba, Sint Eustatius'
+ WHEN 'BG' THEN 'Bulgaria'
+ WHEN 'BF' THEN 'Burkina Faso'
+ WHEN 'BI' THEN 'Burundi'
+ WHEN 'KH' THEN 'Cambodia'
+ WHEN 'CM' THEN 'Cameroon'
+ WHEN 'CA' THEN 'Canada'
+ WHEN 'CV' THEN 'Cape Verde'
+ WHEN 'KY' THEN 'Cayman Islands'
+ WHEN 'CF' THEN 'Central African Republic'
+ WHEN 'TD' THEN 'Chad'
+ WHEN 'CL' THEN 'Chile'
+ WHEN 'CN' THEN 'China'
+ WHEN 'CX' THEN 'Christmas Island'
+ WHEN 'CC' THEN 'Cocos (Keeling) Islands'
+ WHEN 'CO' THEN 'Colombia'
+ WHEN 'KM' THEN 'Comoros'
+ WHEN 'CG' THEN 'Congo'
+ WHEN 'CD' THEN 'Congo, Democratic Republic'
+ WHEN 'CK' THEN 'Cook Islands'
+ WHEN 'CR' THEN 'Costa Rica'
+ WHEN 'CI' THEN 'Cote D\' Ivoire'
+ WHEN 'HR' THEN 'Croatia'
+ WHEN 'CU' THEN 'Cuba'
+ WHEN 'CY' THEN 'Cyprus'
+ WHEN 'CZ' THEN 'Czech Republic'
+ WHEN 'DK' THEN 'Denmark'
+ WHEN 'DJ' THEN 'Djibouti'
+ WHEN 'DM' THEN 'Dominica'
+ WHEN 'DO' THEN 'Dominican Republic'
+ WHEN 'EC' THEN 'Ecuador'
+ WHEN 'EG' THEN 'Egypt'
+ WHEN 'SV' THEN 'El Salvador'
+ WHEN 'GQ' THEN 'Equatorial Guinea'
+ WHEN 'ER' THEN 'Eritrea'
+ WHEN 'EE' THEN 'Estonia'
+ WHEN 'ET' THEN 'Ethiopia'
+ WHEN 'FK' THEN 'Falkland Islands (Malvinas)'
+ WHEN 'FO' THEN 'Faroe Islands'
+ WHEN 'FJ' THEN 'Fiji'
+ WHEN 'FI' THEN 'Finland'
+ WHEN 'FR' THEN 'France'
+ WHEN 'GF' THEN 'French Guiana'
+ WHEN 'PF' THEN 'French Polynesia'
+ WHEN 'TF' THEN 'French Southern Territories'
+ WHEN 'GA' THEN 'Gabon'
+ WHEN 'GM' THEN 'Gambia'
+ WHEN 'GE' THEN 'Georgia'
+ WHEN 'DE' THEN 'Germany'
+ WHEN 'GH' THEN 'Ghana'
+ WHEN 'GI' THEN 'Gibraltar'
+ WHEN 'GR' THEN 'Greece'
+ WHEN 'GL' THEN 'Greenland'
+ WHEN 'GD' THEN 'Grenada'
+ WHEN 'GP' THEN 'Guadeloupe'
+ WHEN 'GU' THEN 'Guam'
+ WHEN 'GT' THEN 'Guatemala'
+ WHEN 'GG' THEN 'Guernsey'
+ WHEN 'GN' THEN 'Guinea'
+ WHEN 'GW' THEN 'Guinea-Bissau'
+ WHEN 'GY' THEN 'Guyana'
+ WHEN 'HT' THEN 'Haiti'
+ WHEN 'HM' THEN 'Heard Island & Mcdonald Islands'
+ WHEN 'VA' THEN 'Holy See (Vatican City State)'
+ WHEN 'HN' THEN 'Honduras'
+ WHEN 'HK' THEN 'Hong Kong'
+ WHEN 'HU' THEN 'Hungary'
+ WHEN 'IS' THEN 'Iceland'
+ WHEN 'IN' THEN 'India'
+ WHEN 'ID' THEN 'Indonesia'
+ WHEN 'IR' THEN 'Iran'
+ WHEN 'IQ' THEN 'Iraq'
+ WHEN 'IE' THEN 'Ireland'
+ WHEN 'IM' THEN 'Isle Of Man'
+ WHEN 'IL' THEN 'Israel'
+ WHEN 'IT' THEN 'Italy'
+ WHEN 'JM' THEN 'Jamaica'
+ WHEN 'JP' THEN 'Japan'
+ WHEN 'JE' THEN 'Jersey'
+ WHEN 'JO' THEN 'Jordan'
+ WHEN 'KZ' THEN 'Kazakhstan'
+ WHEN 'KE' THEN 'Kenya'
+ WHEN 'KI' THEN 'Kiribati'
+ WHEN 'KR' THEN 'Korea'
+ WHEN 'KP' THEN 'North Korea'
+ WHEN 'KW' THEN 'Kuwait'
+ WHEN 'KG' THEN 'Kyrgyzstan'
+ WHEN 'LA' THEN 'Lao People\'s Democratic Republic'
+ WHEN 'LV' THEN 'Latvia'
+ WHEN 'LB' THEN 'Lebanon'
+ WHEN 'LS' THEN 'Lesotho'
+ WHEN 'LR' THEN 'Liberia'
+ WHEN 'LY' THEN 'Libyan Arab Jamahiriya'
+ WHEN 'LI' THEN 'Liechtenstein'
+ WHEN 'LT' THEN 'Lithuania'
+ WHEN 'LU' THEN 'Luxembourg'
+ WHEN 'MO' THEN 'Macao'
+ WHEN 'MK' THEN 'Macedonia'
+ WHEN 'MG' THEN 'Madagascar'
+ WHEN 'MW' THEN 'Malawi'
+ WHEN 'MY' THEN 'Malaysia'
+ WHEN 'MV' THEN 'Maldives'
+ WHEN 'ML' THEN 'Mali'
+ WHEN 'MT' THEN 'Malta'
+ WHEN 'MH' THEN 'Marshall Islands'
+ WHEN 'MQ' THEN 'Martinique'
+ WHEN 'MR' THEN 'Mauritania'
+ WHEN 'MU' THEN 'Mauritius'
+ WHEN 'YT' THEN 'Mayotte'
+ WHEN 'MX' THEN 'Mexico'
+ WHEN 'FM' THEN 'Micronesia, Federated States Of'
+ WHEN 'MD' THEN 'Moldova'
+ WHEN 'MC' THEN 'Monaco'
+ WHEN 'MN' THEN 'Mongolia'
+ WHEN 'ME' THEN 'Montenegro'
+ WHEN 'MS' THEN 'Montserrat'
+ WHEN 'MA' THEN 'Morocco'
+ WHEN 'MZ' THEN 'Mozambique'
+ WHEN 'MM' THEN 'Myanmar'
+ WHEN 'NA' THEN 'Namibia'
+ WHEN 'NR' THEN 'Nauru'
+ WHEN 'NP' THEN 'Nepal'
+ WHEN 'NL' THEN 'Netherlands'
+ WHEN 'AN' THEN 'Netherlands Antilles'
+ WHEN 'NC' THEN 'New Caledonia'
+ WHEN 'NZ' THEN 'New Zealand'
+ WHEN 'NI' THEN 'Nicaragua'
+ WHEN 'NE' THEN 'Niger'
+ WHEN 'NG' THEN 'Nigeria'
+ WHEN 'NU' THEN 'Niue'
+ WHEN 'NF' THEN 'Norfolk Island'
+ WHEN 'MP' THEN 'Northern Mariana Islands'
+ WHEN 'NO' THEN 'Norway'
+ WHEN 'OM' THEN 'Oman'
+ WHEN 'PK' THEN 'Pakistan'
+ WHEN 'PW' THEN 'Palau'
+ WHEN 'PS' THEN 'Palestine'
+ WHEN 'PA' THEN 'Panama'
+ WHEN 'PG' THEN 'Papua New Guinea'
+ WHEN 'PY' THEN 'Paraguay'
+ WHEN 'PE' THEN 'Peru'
+ WHEN 'PH' THEN 'Philippines'
+ WHEN 'PN' THEN 'Pitcairn'
+ WHEN 'PL' THEN 'Poland'
+ WHEN 'PT' THEN 'Portugal'
+ WHEN 'PR' THEN 'Puerto Rico'
+ WHEN 'QA' THEN 'Qatar'
+ WHEN 'RE' THEN 'Reunion'
+ WHEN 'RO' THEN 'Romania'
+ WHEN 'RU' THEN 'Russian Federation'
+ WHEN 'RW' THEN 'Rwanda'
+ WHEN 'BL' THEN 'Saint Barthelemy'
+ WHEN 'SH' THEN 'Saint Helena'
+ WHEN 'KN' THEN 'Saint Kitts And Nevis'
+ WHEN 'LC' THEN 'Saint Lucia'
+ WHEN 'MF' THEN 'Saint Martin'
+ WHEN 'PM' THEN 'Saint Pierre And Miquelon'
+ WHEN 'VC' THEN 'Saint Vincent And Grenadines'
+ WHEN 'WS' THEN 'Samoa'
+ WHEN 'SM' THEN 'San Marino'
+ WHEN 'ST' THEN 'Sao Tome And Principe'
+ WHEN 'SA' THEN 'Saudi Arabia'
+ WHEN 'SN' THEN 'Senegal'
+ WHEN 'RS' THEN 'Serbia'
+ WHEN 'SC' THEN 'Seychelles'
+ WHEN 'SL' THEN 'Sierra Leone'
+ WHEN 'SG' THEN 'Singapore'
+ WHEN 'SK' THEN 'Slovakia'
+ WHEN 'SI' THEN 'Slovenia'
+ WHEN 'SB' THEN 'Solomon Islands'
+ WHEN 'SO' THEN 'Somalia'
+ WHEN 'ZA' THEN 'South Africa'
+ WHEN 'GS' THEN 'South Georgia And Sandwich Isl.'
+ WHEN 'SS' THEN 'South Sudan'
+ WHEN 'ES' THEN 'Spain'
+ WHEN 'LK' THEN 'Sri Lanka'
+ WHEN 'SD' THEN 'Sudan'
+ WHEN 'SR' THEN 'Suriname'
+ WHEN 'SJ' THEN 'Svalbard And Jan Mayen'
+ WHEN 'SZ' THEN 'Swaziland'
+ WHEN 'SE' THEN 'Sweden'
+ WHEN 'CH' THEN 'Switzerland'
+ WHEN 'SY' THEN 'Syrian Arab Republic'
+ WHEN 'TW' THEN 'Taiwan'
+ WHEN 'TJ' THEN 'Tajikistan'
+ WHEN 'TZ' THEN 'Tanzania'
+ WHEN 'TH' THEN 'Thailand'
+ WHEN 'TL' THEN 'Timor-Leste'
+ WHEN 'TG' THEN 'Togo'
+ WHEN 'TK' THEN 'Tokelau'
+ WHEN 'TO' THEN 'Tonga'
+ WHEN 'TT' THEN 'Trinidad And Tobago'
+ WHEN 'TN' THEN 'Tunisia'
+ WHEN 'TR' THEN 'Turkey'
+ WHEN 'TM' THEN 'Turkmenistan'
+ WHEN 'TC' THEN 'Turks And Caicos Islands'
+ WHEN 'TV' THEN 'Tuvalu'
+ WHEN 'UG' THEN 'Uganda'
+ WHEN 'UA' THEN 'Ukraine'
+ WHEN 'AE' THEN 'United Arab Emirates'
+ WHEN 'GB' THEN 'United Kingdom'
+ WHEN 'US' THEN 'United States'
+ WHEN 'UM' THEN 'United States Outlying Islands'
+ WHEN 'UY' THEN 'Uruguay'
+ WHEN 'UZ' THEN 'Uzbekistan'
+ WHEN 'VU' THEN 'Vanuatu'
+ WHEN 'VE' THEN 'Venezuela'
+ WHEN 'VN' THEN 'Vietnam'
+ WHEN 'VG' THEN 'Virgin Islands, British'
+ WHEN 'VI' THEN 'Virgin Islands, U.S.'
+ WHEN 'WF' THEN 'Wallis And Futuna'
+ WHEN 'EH' THEN 'Western Sahara'
+ WHEN 'YE' THEN 'Yemen'
+ WHEN 'ZM' THEN 'Zambia'
+ WHEN 'ZW' THEN 'Zimbabwe'
+ ELSE ''
+ END AS country_name";
+
+}
diff --git a/src/Features/Export/StreamingFileResult.cs b/src/Features/Export/StreamingFileResult.cs
new file mode 100755
index 0000000..0e4bd58
--- /dev/null
+++ b/src/Features/Export/StreamingFileResult.cs
@@ -0,0 +1,21 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Aptabase.Features.Export;
+
+public class StreamingFileResult(Func streamContent, string contentType, string fileName) : IActionResult
+{
+ private readonly Func _streamContent = streamContent;
+ private readonly string _contentType = contentType;
+ private readonly string _fileName = fileName;
+
+ public async Task ExecuteResultAsync(ActionContext context)
+ {
+ var httpContext = context.HttpContext;
+ var response = httpContext.Response;
+
+ response.Headers.TryAdd("Content-Disposition", new[] { $"attachment; filename={_fileName}" });
+ response.ContentType = _contentType;
+
+ await _streamContent(response.Body, httpContext, httpContext.RequestAborted);
+ }
+}
diff --git a/src/Features/FeatureFlags/FeatureFlag.cs b/src/Features/FeatureFlags/FeatureFlag.cs
new file mode 100755
index 0000000..0cc8bfc
--- /dev/null
+++ b/src/Features/FeatureFlags/FeatureFlag.cs
@@ -0,0 +1,11 @@
+namespace Aptabase.Features.FeatureFlags
+{
+ public class FeatureFlag
+ {
+ public string? Id { get; set; }
+ public string? AppId { get; set; }
+ public string Key { get; set; } = "";
+ public string Value { get; set; } = "";
+ public string Environment { get; set; } = "";
+ }
+}
diff --git a/src/Features/FeatureFlags/FeatureFlagStateController.cs b/src/Features/FeatureFlags/FeatureFlagStateController.cs
new file mode 100755
index 0000000..2ad2078
--- /dev/null
+++ b/src/Features/FeatureFlags/FeatureFlagStateController.cs
@@ -0,0 +1,105 @@
+using Aptabase.Data;
+using Aptabase.Features.Ingestion;
+using Dapper;
+using Microsoft.AspNetCore.Cors;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.RateLimiting;
+using System.Text.Json;
+
+namespace Aptabase.Features.FeatureFlags;
+
+public class GetFeatureFlagBody
+{
+ public SystemProperties SystemProps { get; set; } = new();
+ public JsonDocument? Props { get; set; }
+}
+
+[ApiController]
+[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
+public class FeatureFlagStateController : Controller
+{
+ private readonly ILogger _logger;
+ private readonly IIngestionCache _cache;
+ private readonly IDbContext _db;
+
+ public FeatureFlagStateController(
+ IDbContext db,
+ IIngestionCache cache,
+ ILogger logger)
+ {
+ _db = db ?? throw new ArgumentNullException(nameof(db));
+ _cache = cache ?? throw new ArgumentNullException(nameof(cache));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ [HttpGet("/api/v0/feature-flags/{featureFlagKey}")]
+ [EnableCors("AllowAny")]
+ [EnableRateLimiting("FeatureFlags")]
+ public async Task Single(
+ string featureFlagKey,
+ [FromHeader(Name = "App-Key")] string? appKey,
+ [FromHeader(Name = "User-Agent")] string? userAgent,
+ [FromBody] GetFeatureFlagBody body)
+ {
+ appKey = appKey?.ToUpper() ?? "";
+
+ var app = await _cache.FindByAppKey(appKey, HttpContext.RequestAborted);
+
+ if (string.IsNullOrEmpty(app.Id))
+ {
+ return NotFound($"Appplication not found with given app key: {appKey}");
+ }
+
+ if (app.IsLocked)
+ {
+ return BadRequest($"Owner account is locked.");
+ }
+
+ var flag = await _db.Connection.QueryFirstOrDefaultAsync(@"
+ SELECT f.key, f.value
+ FROM feature_flags f
+ WHERE f.app_id = @appId and f.key = @key",
+ new
+ {
+ appId = app.Id,
+ key = featureFlagKey,
+ });
+
+ return Ok(flag);
+ }
+
+ [HttpGet("/api/v0/feature-flags")]
+ [EnableCors("AllowAny")]
+ [EnableRateLimiting("FeatureFlags")]
+ public async Task All(
+ [FromHeader(Name = "App-Key")] string? appKey,
+ [FromHeader(Name = "User-Agent")] string? userAgent,
+ [FromBody] GetFeatureFlagBody body)
+ {
+ appKey = appKey?.ToUpper() ?? "";
+
+ var app = await _cache.FindByAppKey(appKey, HttpContext.RequestAborted);
+
+ if (string.IsNullOrEmpty(app.Id))
+ {
+ return NotFound($"Appplication not found with given app key: {appKey}");
+ }
+
+ if (app.IsLocked)
+ {
+ return BadRequest($"Owner account is locked.");
+ }
+
+ var flags = await _db.Connection.QueryAsync(@"
+ SELECT f.key, f.value
+ FROM feature_flags f
+ WHERE f.app_id = @appId
+ LIMIT 50",
+ new
+ {
+ appId = app.Id,
+ });
+
+ return Ok(flags);
+ }
+}
diff --git a/src/Features/FeatureFlags/FeatureFlagsController.cs b/src/Features/FeatureFlags/FeatureFlagsController.cs
new file mode 100755
index 0000000..1d59bc0
--- /dev/null
+++ b/src/Features/FeatureFlags/FeatureFlagsController.cs
@@ -0,0 +1,173 @@
+using Aptabase.Data;
+using Aptabase.Features.Apps;
+using Aptabase.Features.Authentication;
+using Dapper;
+using Microsoft.AspNetCore.Mvc;
+using System.ComponentModel.DataAnnotations;
+
+namespace Aptabase.Features.FeatureFlags;
+
+public class CreateFeatureFlagRequestBody : UpdateFeatureFlagRequestBody
+{
+ public string AppId { get; set; } = "";
+}
+
+public class UpdateFeatureFlagRequestBody
+{
+ [Required]
+ [StringLength(255, MinimumLength = 2)]
+ public string Key { get; set; } = "";
+
+ [Required]
+ [StringLength(4000, MinimumLength = 1)]
+ public string Value { get; set; } = "";
+
+ [Required]
+ [StringLength(50, MinimumLength = 2)]
+ public string Environment { get; set; } = "";
+}
+
+[ApiController, IsAuthenticated]
+[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
+public class FeatureFlagsController : Controller
+{
+ private readonly IDbContext _db;
+ private readonly IAppQueries _appQueries;
+
+ public FeatureFlagsController(IDbContext db, IAppQueries appQueries)
+ {
+ _db = db ?? throw new ArgumentNullException(nameof(db));
+ _appQueries = appQueries ?? throw new ArgumentNullException(nameof(appQueries));
+ }
+
+ [HttpGet("/api/_flags/{appId}")]
+ public async Task List(string appId)
+ {
+ var user = this.GetCurrentUserIdentity();
+
+ var app = await _appQueries.GetOwnedAppAsync(appId, user.Id);
+
+ if (app == null)
+ {
+ return NotFound();
+ }
+
+ var flags = await _db.Connection.QueryAsync(@"
+ SELECT f.id, f.app_id, f.key, f.value, f.environment, f.conditions
+ FROM feature_flags f
+ WHERE f.app_id = @appId
+ LIMIT 50",
+ new
+ {
+ appId,
+ });
+
+ return Ok(flags);
+ }
+
+ [HttpPost("/api/_flags")]
+ public async Task Create([FromBody] CreateFeatureFlagRequestBody body)
+ {
+ var user = this.GetCurrentUserIdentity();
+
+ var app = await _appQueries.GetOwnedAppAsync(body.AppId, user.Id);
+
+ if (app == null)
+ {
+ return NotFound();
+ }
+
+ var flag = new FeatureFlag
+ {
+ Id = NanoId.New(),
+ AppId = body.AppId,
+ Key = body.Key,
+ Value = body.Value,
+ Environment = body.Environment,
+ };
+
+ await _db.Connection.ExecuteAsync(@"
+ INSERT INTO feature_flags (id, app_id, key, value, environment, created_at, modified_at)
+ VALUES (@id, @appId, @key, @value, @environment, now(), now())",
+ new
+ {
+ id = flag.Id,
+ appId = flag.AppId,
+ key = flag.Key,
+ value = flag.Value,
+ environment = flag.Environment,
+ });
+
+ return Ok(app);
+ }
+
+ [HttpPut("/api/_flags/{flagId}")]
+ public async Task Update(string flagId, [FromBody] UpdateFeatureFlagRequestBody body)
+ {
+ var user = this.GetCurrentUserIdentity();
+
+ var flag = await GetOwnedFeatureFlagAsync(flagId, user.Id);
+
+ if (flag == null)
+ {
+ return NotFound();
+ }
+
+ flag.Key = body.Key;
+ flag.Value = body.Value;
+ flag.Environment = body.Environment;
+
+ await _db.Connection.ExecuteAsync(
+ "UPDATE feature_flags SET Key = @key, Value = @value, Environment = @environment, WHERE id = @flagId",
+ new
+ {
+ flagId,
+ key = flag.Key,
+ value = flag.Value,
+ environment = flag.Environment,
+ });
+
+ return Ok(flag);
+ }
+
+ [HttpDelete("/api/_flags/{flagId}")]
+ public async Task Delete(string flagId)
+ {
+ var user = this.GetCurrentUserIdentity();
+
+ var flag = await GetOwnedFeatureFlagAsync(flagId, user.Id);
+
+ if (flag == null)
+ {
+ return NotFound();
+ }
+
+ await _db.Connection.ExecuteAsync(
+ "DELETE FROM feature_flags WHERE id = @flagId",
+ new
+ {
+ flagId,
+ });
+
+ return NoContent();
+ }
+
+ private Task GetOwnedFeatureFlagAsync(string flagId, string userId)
+ {
+ return _db.Connection.QueryFirstOrDefaultAsync(@"
+ SELECT f.id, f.app_id, f.key, f.value, f.environment, f.conditions
+ FROM feature_flags f
+ JOIN apps a
+ ON f.app_id = a.id
+ INNER JOIN users u
+ ON u.id = a.owner_id
+ WHERE f.id = @flagId
+ AND a.owner_id = @userId
+ AND a.deleted_at IS NULL",
+ new
+ {
+ flagId,
+ userId,
+ });
+ }
+}
diff --git a/src/Features/GeoIP/CloudGeoClient.cs b/src/Features/GeoIP/CloudGeoClient.cs
new file mode 100755
index 0000000..434cb20
--- /dev/null
+++ b/src/Features/GeoIP/CloudGeoClient.cs
@@ -0,0 +1,44 @@
+using System.Text.Json;
+
+namespace Aptabase.Features.GeoIP;
+
+public class CloudGeoClient : GeoIPClient
+{
+ private readonly Dictionary _regions;
+
+ public CloudGeoClient(EnvSettings env)
+ : base(env)
+ {
+ var text = File.ReadAllText(Path.Combine(env.EtcDirectoryPath, "geoip/iso3166-2.json"));
+ var regions = JsonSerializer.Deserialize>(text);
+ if (regions == null)
+ throw new Exception("Failed to deserialize geoip/iso3166-2.json");
+
+ _regions = regions;
+ }
+
+ public override GeoLocation GetClientLocation(HttpContext httpContext)
+ {
+ var countryCode = GetHeader(httpContext, "cdn-requestcountrycode", "CloudFront-Viewer-Country", "CF-IPCountry");
+ var regionCode = GetHeader(httpContext, "cdn-requeststatecode", "Cloudfront-Viewer-Country-Region");
+ var regionName = _regions.TryGetValue($"{countryCode}-{regionCode}", out var name) ? name : "";
+
+ return new GeoLocation
+ {
+ CountryCode = countryCode ?? "",
+ RegionName = regionName ?? ""
+ };
+ }
+
+ private static string GetHeader(HttpContext httpContext, params string[] names)
+ {
+ foreach (var name in names)
+ {
+ var value = httpContext.Request.Headers[name].ToString()?.ToUpper() ?? "";
+ if (!string.IsNullOrEmpty(value))
+ return value;
+ }
+
+ return "";
+ }
+}
\ No newline at end of file
diff --git a/src/Features/GeoIP/DatabaseGeoClient.cs b/src/Features/GeoIP/DatabaseGeoClient.cs
new file mode 100755
index 0000000..e12f288
--- /dev/null
+++ b/src/Features/GeoIP/DatabaseGeoClient.cs
@@ -0,0 +1,28 @@
+using MaxMind.GeoIP2;
+
+namespace Aptabase.Features.GeoIP;
+
+public class DatabaseGeoClient : GeoIPClient
+{
+ private readonly DatabaseReader _db;
+
+ public DatabaseGeoClient(EnvSettings env)
+ : base(env)
+ {
+ _db = new DatabaseReader(Path.Combine(env.EtcDirectoryPath, "geoip/GeoLite2-City.mmdb"));
+ }
+
+ public override GeoLocation GetClientLocation(HttpContext httpContext)
+ {
+ var ip = httpContext.ResolveClientIpAddress();
+
+ if (string.IsNullOrEmpty(ip))
+ return GeoLocation.Empty;
+
+ return _db.TryCity(ip, out var city) ? new GeoLocation
+ {
+ CountryCode = city?.Country?.IsoCode?.ToUpper() ?? "",
+ RegionName = city?.MostSpecificSubdivision.Name ?? ""
+ } : GeoLocation.Empty;
+ }
+}
\ No newline at end of file
diff --git a/src/Features/GeoIP/GeoIPExtensions.cs b/src/Features/GeoIP/GeoIPExtensions.cs
new file mode 100755
index 0000000..13c8b27
--- /dev/null
+++ b/src/Features/GeoIP/GeoIPExtensions.cs
@@ -0,0 +1,15 @@
+namespace Aptabase.Features.GeoIP;
+
+public static class GeoIPExtensions
+{
+ public static void AddGeoIPClient(this IServiceCollection services, EnvSettings env)
+ {
+ if (env.IsManagedCloud)
+ {
+ services.AddSingleton();
+ return;
+ }
+
+ services.AddSingleton();
+ }
+}
\ No newline at end of file
diff --git a/src/Features/GeoIP/IGeoIPClient.cs b/src/Features/GeoIP/IGeoIPClient.cs
new file mode 100755
index 0000000..1a7825b
--- /dev/null
+++ b/src/Features/GeoIP/IGeoIPClient.cs
@@ -0,0 +1,54 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Aptabase.Features.GeoIP;
+
+public readonly struct GeoLocation
+{
+ public readonly string CountryCode { get; init; }
+ public readonly string RegionName { get; init; }
+
+ public static GeoLocation Empty => new()
+ {
+ CountryCode = "",
+ RegionName = ""
+ };
+}
+
+public readonly struct Coordinates
+{
+ [JsonPropertyName("lat")]
+ public readonly double Latitude { get; init; }
+ [JsonPropertyName("lng")]
+ public readonly double Longitude { get; init; }
+}
+
+public abstract class GeoIPClient
+{
+ public abstract GeoLocation GetClientLocation(HttpContext httpContext);
+
+ private readonly Dictionary> _coordinates;
+
+ public GeoIPClient(EnvSettings env)
+ {
+ var text = File.ReadAllText(Path.Combine(env.EtcDirectoryPath, "geoip/coordinates.json"));
+ var coordinates = JsonSerializer.Deserialize>>(text);
+ if (coordinates == null)
+ throw new Exception("Failed to deserialize coordinates.json");
+
+ _coordinates = coordinates;
+ }
+
+ public (double, double) GetLatLng(string countryCode, string regionName)
+ {
+ if (_coordinates.TryGetValue(countryCode, out var regions))
+ {
+ if (regions?.TryGetValue(regionName, out var coordinates) == true)
+ {
+ return (coordinates.Latitude, coordinates.Longitude);
+ }
+ }
+
+ return (0, 0);
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Ingestion/Buffer/ClickHouseIngestionClient.cs b/src/Features/Ingestion/Buffer/ClickHouseIngestionClient.cs
new file mode 100755
index 0000000..d3d19ad
--- /dev/null
+++ b/src/Features/Ingestion/Buffer/ClickHouseIngestionClient.cs
@@ -0,0 +1,100 @@
+using Dapper;
+using ClickHouse.Client.Copy;
+using ClickHouse.Client.ADO;
+
+namespace Aptabase.Features.Ingestion.Buffer;
+
+public class ClickHouseIngestionClient : IIngestionClient
+{
+ private readonly ClickHouseConnection _conn;
+
+ private readonly string[] COLUMNS = [
+ "app_id",
+ "timestamp",
+ "event_name",
+ "user_id",
+ "session_id",
+ "os_name",
+ "os_version",
+ "device_model",
+ "locale",
+ "app_version",
+ "app_build_number",
+ "engine_name",
+ "engine_version",
+ "sdk_version",
+ "country_code",
+ "region_name",
+ "city",
+ "string_props",
+ "numeric_props",
+ "ttl"
+ ];
+
+ public ClickHouseIngestionClient(ClickHouseConnection conn, ILogger logger)
+ {
+ _conn = conn ?? throw new ArgumentNullException(nameof(conn));
+ }
+
+ public async Task SendEventAsync(EventRow row)
+ {
+ return await _conn.ExecuteAsync($@"INSERT INTO events ({string.Join(",", COLUMNS)}) VALUES (@{string.Join(", @", COLUMNS)})", new {
+ app_id = row.AppId,
+ timestamp = row.Timestamp,
+ event_name = row.EventName,
+ user_id = row.UserId,
+ session_id = row.SessionId,
+ os_name = row.OSName,
+ os_version = row.OSVersion,
+ device_model = row.DeviceModel,
+ locale = row.Locale,
+ app_version = row.AppVersion,
+ app_build_number = row.AppBuildNumber,
+ engine_name = row.EngineName,
+ engine_version = row.EngineVersion,
+ sdk_version = row.SdkVersion,
+ country_code = row.CountryCode,
+ region_name = row.RegionName,
+ city = row.City,
+ string_props = row.StringProps,
+ numeric_props = row.NumericProps,
+ ttl = row.TTL,
+ });
+ }
+
+ public async Task BulkSendEventAsync(IEnumerable rows, CancellationToken ct = default)
+ {
+ using var bulkCopy = new ClickHouseBulkCopy(_conn)
+ {
+ DestinationTableName = "events",
+ BatchSize = 1000,
+ ColumnNames = COLUMNS,
+ };
+
+ var values = rows.Select(row => new object[] {
+ row.AppId,
+ row.Timestamp,
+ row.EventName,
+ row.UserId,
+ row.SessionId,
+ row.OSName,
+ row.OSVersion,
+ row.DeviceModel,
+ row.Locale,
+ row.AppVersion,
+ row.AppBuildNumber,
+ row.EngineName,
+ row.EngineVersion,
+ row.SdkVersion,
+ row.CountryCode,
+ row.RegionName,
+ row.City,
+ row.StringProps,
+ row.NumericProps,
+ row.TTL,
+ });
+ await bulkCopy.InitAsync();
+ await bulkCopy.WriteToServerAsync(values, ct);
+ return bulkCopy.RowsWritten;
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Ingestion/Buffer/EventBackgroundWritter.cs b/src/Features/Ingestion/Buffer/EventBackgroundWritter.cs
new file mode 100755
index 0000000..3bd2146
--- /dev/null
+++ b/src/Features/Ingestion/Buffer/EventBackgroundWritter.cs
@@ -0,0 +1,75 @@
+
+
+using System.Diagnostics;
+using Aptabase.Features.Privacy;
+
+namespace Aptabase.Features.Ingestion.Buffer;
+
+public class EventBackgroundWritter : BackgroundService
+{
+ private readonly IEventBuffer _buffer;
+ private readonly IIngestionClient _client;
+ private readonly ILogger _logger;
+ private readonly IUserHasher _hasher;
+ private readonly Stopwatch _watch = new();
+
+ public EventBackgroundWritter(IEventBuffer buffer, IUserHasher hasher, IIngestionClient client, ILogger logger)
+ {
+ _hasher = hasher ?? throw new ArgumentNullException(nameof(hasher));
+ _buffer = buffer ?? throw new ArgumentNullException(nameof(buffer));
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ _logger.LogInformation("EventBackgroundWritter is starting.");
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ try
+ {
+ await FlushEvents();
+ await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
+ }
+ catch { }
+ }
+
+ // We need to wait a few seconds when cancellation is requested
+ // because some events may be added to the buffer after the cancellation
+ // After flushing we can safely exit
+ _logger.LogInformation("EventBackgroundWritter is stopping.");
+ await Task.Delay(TimeSpan.FromSeconds(2));
+ await FlushEvents();
+ _logger.LogInformation("EventBackgroundWritter stopped.");
+ }
+
+ public int Count() => _buffer.TakeAll().Length;
+
+ public async Task FlushEvents()
+ {
+ var events = _buffer.TakeAll();
+ if (events.Length == 0) return;
+
+ try
+ {
+ _watch.Restart();
+
+ var rows = await Task.WhenAll(events.Select(ToEventRow));
+
+ await _client.BulkSendEventAsync(rows);
+ _watch.Stop();
+ _logger.LogInformation("Flushed {Count} events in {TimeMs}ms.", events.Length, _watch.ElapsedMilliseconds);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to send events. {Count} events were discarded.", events.Length);
+ }
+ }
+
+ private async Task ToEventRow(TrackingEvent e)
+ {
+ var userId = await _hasher.CalculateHash(e.Timestamp, e.AppId, e.SessionId, e.ClientIpAddress, e.UserAgent);
+ return new EventRow(ref e, userId);
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Ingestion/Buffer/EventRow.cs b/src/Features/Ingestion/Buffer/EventRow.cs
new file mode 100755
index 0000000..2c495f9
--- /dev/null
+++ b/src/Features/Ingestion/Buffer/EventRow.cs
@@ -0,0 +1,89 @@
+using System.Text.Json;
+
+namespace Aptabase.Features.Ingestion.Buffer;
+
+public readonly struct EventRow
+{
+ // Events from Debug builds are kept for 6 months
+ public static readonly TimeSpan DebugTTL = TimeSpan.FromDays(182);
+ // Events from Release builds are kept for 5 years
+ public static readonly TimeSpan ReleaseTTL = TimeSpan.FromDays(5 * 365);
+
+ public readonly string AppId;
+ public readonly DateTime Timestamp;
+ public readonly string EventName;
+ public readonly string UserId;
+ public readonly string SessionId;
+ public readonly string OSName;
+ public readonly string OSVersion;
+ public readonly string DeviceModel;
+ public readonly string Locale;
+ public readonly string AppVersion;
+ public readonly string AppBuildNumber;
+ public readonly string EngineName;
+ public readonly string EngineVersion;
+ public readonly string SdkVersion;
+ public readonly string CountryCode;
+ public readonly string RegionName;
+ public readonly string City;
+ public readonly string StringProps;
+ public readonly string NumericProps;
+ public readonly DateTime TTL;
+
+ public EventRow(ref TrackingEvent e, string userId)
+ {
+ var ttl = e.IsDebug ? DebugTTL : ReleaseTTL;
+
+ AppId = e.IsDebug ? $"{e.AppId}_DEBUG" : e.AppId;
+ Timestamp = e.Timestamp;
+ EventName = e.EventName;
+ UserId = userId;
+ SessionId = e.SessionId;
+ OSName = e.OSName;
+ OSVersion = e.OSVersion;
+ DeviceModel = e.DeviceModel;
+ Locale = e.Locale;
+ AppVersion = e.AppVersion;
+ AppBuildNumber = e.AppBuildNumber;
+ EngineName = e.EngineName;
+ EngineVersion = e.EngineVersion;
+ SdkVersion = e.SdkVersion;
+ CountryCode = e.CountryCode;
+ RegionName = e.RegionName;
+ City = "";
+ StringProps = e.StringProps;
+ NumericProps = e.NumericProps;
+ TTL = e.Timestamp.Add(ttl);
+ }
+
+ public void WriteJson(StringWriter writer)
+ {
+ writer.Write("{");
+ WriteProperty(writer, "appId", AppId);
+ WriteProperty(writer, "timestamp", Timestamp.ToString("o"));
+ WriteProperty(writer, "eventName", EventName);
+ WriteProperty(writer, "userId", UserId);
+ WriteProperty(writer, "sessionId", SessionId);
+ WriteProperty(writer, "osName", OSName);
+ WriteProperty(writer, "osVersion", OSVersion);
+ WriteProperty(writer, "deviceModel", DeviceModel);
+ WriteProperty(writer, "locale", Locale);
+ WriteProperty(writer, "appVersion", AppVersion);
+ WriteProperty(writer, "appBuildNumber", AppBuildNumber);
+ WriteProperty(writer, "engineName", EngineName);
+ WriteProperty(writer, "engineVersion", EngineVersion);
+ WriteProperty(writer, "sdkVersion", SdkVersion);
+ WriteProperty(writer, "countryCode", CountryCode);
+ WriteProperty(writer, "regionName", RegionName);
+ WriteProperty(writer, "city", "");
+ WriteProperty(writer, "stringProps", StringProps);
+ WriteProperty(writer, "numericProps", NumericProps);
+ WriteProperty(writer, "ttl", TTL.ToString("o"), true);
+ writer.Write("}");
+ }
+
+ private static void WriteProperty(StringWriter writer, string name, string value, bool isLast = false)
+ {
+ writer.Write($"\"{name}\": \"{JsonEncodedText.Encode(value)}\" {(isLast ? "" : ",")}");
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Ingestion/Buffer/IIngestionClient.cs b/src/Features/Ingestion/Buffer/IIngestionClient.cs
new file mode 100755
index 0000000..98c77cc
--- /dev/null
+++ b/src/Features/Ingestion/Buffer/IIngestionClient.cs
@@ -0,0 +1,8 @@
+namespace Aptabase.Features.Ingestion.Buffer;
+
+public interface IIngestionClient
+{
+ Task SendEventAsync(EventRow row);
+ Task BulkSendEventAsync(IEnumerable rows, CancellationToken ct = default);
+}
+
diff --git a/src/Features/Ingestion/Buffer/InMemoryEventBuffer.cs b/src/Features/Ingestion/Buffer/InMemoryEventBuffer.cs
new file mode 100755
index 0000000..2849af6
--- /dev/null
+++ b/src/Features/Ingestion/Buffer/InMemoryEventBuffer.cs
@@ -0,0 +1,49 @@
+namespace Aptabase.Features.Ingestion.Buffer;
+
+public interface IEventBuffer
+{
+ void Add(ref TrackingEvent @event);
+ void AddRange(ref TrackingEvent[] events);
+ void AddRange(ref IEnumerable events);
+ TrackingEvent[] TakeAll();
+}
+
+public class InMemoryEventBuffer : IEventBuffer
+{
+ private List _buffer = new();
+ private object _lock = new object();
+
+ public void Add(ref TrackingEvent @event)
+ {
+ lock (_lock)
+ {
+ _buffer.Add(@event);
+ }
+ }
+
+ public void AddRange(ref TrackingEvent[] events)
+ {
+ lock (_lock)
+ {
+ _buffer.AddRange(events);
+ }
+ }
+
+ public void AddRange(ref IEnumerable events)
+ {
+ lock (_lock)
+ {
+ _buffer.AddRange(events);
+ }
+ }
+
+ public TrackingEvent[] TakeAll()
+ {
+ lock (_lock)
+ {
+ var items = _buffer.ToArray();
+ _buffer.Clear();
+ return items;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Ingestion/Buffer/TinybirdIngestionClient.cs b/src/Features/Ingestion/Buffer/TinybirdIngestionClient.cs
new file mode 100755
index 0000000..f61c069
--- /dev/null
+++ b/src/Features/Ingestion/Buffer/TinybirdIngestionClient.cs
@@ -0,0 +1,77 @@
+using System.Text.Json.Serialization;
+
+namespace Aptabase.Features.Ingestion.Buffer;
+
+public class InsertResult
+{
+ [JsonPropertyName("successful_rows")]
+ public int SuccessfulRows { get; set; }
+ [JsonPropertyName("quarantined_rows")]
+ public int QuarantinedRows { get; set; }
+}
+
+public class TinybirdIngestionClient : IIngestionClient
+{
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger;
+ private readonly TimeSpan[] _retriesDelay =
+ [
+ TimeSpan.FromMilliseconds(1000),
+ TimeSpan.FromMilliseconds(3000),
+ TimeSpan.FromMilliseconds(5000),
+ ];
+
+ public TinybirdIngestionClient(IHttpClientFactory factory, ILogger logger)
+ {
+ _httpClient = factory.CreateClient("Tinybird");
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ private static string EventsPath => $"/v0/events?name=events&wait=true";
+
+ public Task SendEventAsync(EventRow row)
+ {
+ return PostAsync(EventsPath, [row]);
+ }
+
+ public Task BulkSendEventAsync(IEnumerable rows, CancellationToken ct = default)
+ {
+ return PostAsync(EventsPath, rows, ct);
+ }
+
+ private async Task PostAsync(string path, IEnumerable rows, CancellationToken ct = default)
+ {
+ using var content = SerializeBody(rows);
+
+ for (var i = 0; i < _retriesDelay.Length; i++)
+ {
+ try
+ {
+ var response = await _httpClient.PostAsync(path, content, ct);
+ response.EnsureSuccessStatusCode();
+
+ var result = await response.Content.ReadFromJsonAsync() ?? new InsertResult();
+ return result.SuccessfulRows;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to send events to Tinybird. Will retry again after {Delay}ms.", _retriesDelay[i].TotalMilliseconds);
+ await Task.Delay(_retriesDelay[i]);
+ }
+ }
+
+ throw new Exception($"Failed to send events to Tinybird after {_retriesDelay.Length} retries.");
+ }
+
+ private static StringContent SerializeBody(IEnumerable rows)
+ {
+ using var writer = new StringWriter();
+ foreach (var row in rows)
+ {
+ row.WriteJson(writer);
+ writer.Write("\n");
+ }
+
+ return new StringContent(writer.ToString());
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Ingestion/Buffer/TrackingEvent.cs b/src/Features/Ingestion/Buffer/TrackingEvent.cs
new file mode 100755
index 0000000..d28f71e
--- /dev/null
+++ b/src/Features/Ingestion/Buffer/TrackingEvent.cs
@@ -0,0 +1,28 @@
+using System.Text.Json;
+
+namespace Aptabase.Features.Ingestion;
+
+public readonly struct TrackingEvent
+{
+ public readonly string ClientIpAddress { get; init; }
+ public readonly string UserAgent { get; init; }
+
+ public readonly string AppId { get; init; }
+ public readonly DateTime Timestamp { get; init; }
+ public readonly string EventName { get; init; }
+ public readonly string SessionId { get; init; }
+ public readonly string OSName { get; init; }
+ public readonly string OSVersion { get; init; }
+ public readonly string DeviceModel { get; init; }
+ public readonly string Locale { get; init; }
+ public readonly string AppVersion { get; init; }
+ public readonly string AppBuildNumber { get; init; }
+ public readonly string EngineName { get; init; }
+ public readonly string EngineVersion { get; init; }
+ public readonly string SdkVersion { get; init; }
+ public readonly string CountryCode { get; init; }
+ public readonly string RegionName { get; init; }
+ public readonly string StringProps { get; init; }
+ public readonly string NumericProps { get; init; }
+ public readonly bool IsDebug { get; init; }
+}
\ No newline at end of file
diff --git a/src/Features/Ingestion/EventBody.cs b/src/Features/Ingestion/EventBody.cs
new file mode 100755
index 0000000..8750be9
--- /dev/null
+++ b/src/Features/Ingestion/EventBody.cs
@@ -0,0 +1,193 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace Aptabase.Features.Ingestion;
+
+public class SystemProperties
+{
+ public bool IsDebug { get; set; }
+
+ [StringLength(30)]
+ public string? OSName { get; set; }
+
+ [StringLength(100)]
+ public string? OSVersion { get; set; }
+
+ [StringLength(10)]
+ public string? Locale { get; set; }
+
+ [StringLength(50)]
+ public string? AppVersion { get; set; }
+
+ [StringLength(20)]
+ public string? AppBuildNumber { get; set; }
+
+ [StringLength(30)]
+ public string? EngineName { get; set; }
+
+ [StringLength(30)]
+ public string? EngineVersion { get; set; }
+
+ [Required, StringLength(40)]
+ public string SdkVersion { get; set; } = "";
+
+ [StringLength(100)]
+ public string? DeviceModel { get; set; }
+}
+
+public class EventBody
+{
+ [Required, StringLength(60)]
+ public string EventName { get; set; } = "";
+
+ private DateTime _ts;
+ public DateTime Timestamp
+ {
+ get => _ts;
+ set => _ts = value > DateTime.UtcNow ? DateTime.UtcNow : value;
+ }
+
+ [Required]
+ public string SessionId { get; set; } = "";
+
+ public SystemProperties SystemProps { get; set; } = new();
+ public JsonDocument? Props { get; set; }
+
+ public (JsonObject, JsonObject) SplitProps()
+ {
+ var stringValues = new JsonObject();
+ var numericValues = new JsonObject();
+
+ if (Props != null)
+ {
+ // Sort by key to ensure consistent order might be useful in future!
+ foreach (var property in Props.RootElement.EnumerateObject().OrderBy(x => x.Name))
+ {
+ if (property.Value.ValueKind == JsonValueKind.Number)
+ numericValues.Add(property.Name, property.Value.GetDecimal());
+ else if (property.Value.ValueKind == JsonValueKind.String)
+ {
+ var propertyValue = property.Value.GetString() ?? "";
+ stringValues.Add(property.Name, propertyValue.Truncate(180, "..."));
+ }
+ else if (property.Value.ValueKind == JsonValueKind.True)
+ stringValues.Add(property.Name, "true");
+ else if (property.Value.ValueKind == JsonValueKind.False)
+ stringValues.Add(property.Name, "false");
+ else if (property.Value.ValueKind == JsonValueKind.Array)
+ stringValues.Add(property.Name, "[Array]");
+ else if (property.Value.ValueKind == JsonValueKind.Object)
+ stringValues.Add(property.Name, "{Object}");
+ else if (property.Value.ValueKind == JsonValueKind.Null)
+ stringValues.Add(property.Name, "");
+ else if (property.Value.ValueKind == JsonValueKind.Undefined)
+ stringValues.Add(property.Name, "");
+ }
+ }
+
+ return (stringValues, numericValues);
+ }
+
+ public (bool, string) IsValid(ILogger logger)
+ {
+ var (valid, msg) = ValidateSessionId(logger);
+ if (!valid)
+ return (false, msg);
+
+ if (Timestamp > DateTime.UtcNow.AddMinutes(10))
+ return (false, "Future events are not allowed.");
+
+ if (Timestamp < DateTime.UtcNow.AddDays(-1))
+ {
+ logger.LogWarning("Event timestamp {EventTimestamp} is too old.", Timestamp);
+ return (false, "Event is too old.");
+ }
+
+ var locale = LocaleFormatter.FormatLocale(SystemProps.Locale);
+ if (locale is null)
+ logger.LogWarning("Invalid locale {Locale} received from {OS} using {SdkVersion}", locale, SystemProps.OSName, SystemProps.SdkVersion);
+
+ SystemProps.Locale = locale;
+
+ (valid, msg) = ValidateProps();
+ if (!valid)
+ return (false, msg);
+
+ return (true, string.Empty);
+ }
+
+ private (bool, string) ValidateSessionId(ILogger logger)
+ {
+ if (ulong.TryParse(SessionId, out var numericSessionId))
+ return ValidateNumericSessionId(numericSessionId, logger);
+
+ if (SessionId.Length > 36)
+ return (false, $"SessionId must be less than or equal to 36 characters, was: {SessionId}");
+
+ return (true, string.Empty);
+ }
+
+ private (bool, string) ValidateNumericSessionId(ulong id, ILogger logger)
+ {
+ var secondsSinceEpoch = id / 100_000_000;
+ var sessionStartedAt = DateTimeOffset.FromUnixTimeSeconds((long)secondsSinceEpoch).UtcDateTime;
+
+ if (sessionStartedAt > DateTime.UtcNow.AddMinutes(10))
+ {
+ logger.LogWarning("Session {SessionId} timestamp {StartedAt} is in future, received from {SdkVersion}.", id, sessionStartedAt, SystemProps.SdkVersion);
+ return (false, "Future sessions are not allowed.");
+ }
+
+ if (sessionStartedAt < DateTime.UtcNow.AddDays(-7))
+ {
+ logger.LogWarning("Session {SessionId} timestamp {StartedAt} is too old, received from {SdkVersion}.", id, sessionStartedAt, SystemProps.SdkVersion);
+ return (false, "Session is too old.");
+ }
+
+ return (true, string.Empty);
+ }
+
+ private (bool, string) ValidateProps()
+ {
+ if (Props is not null)
+ {
+ if (Props.RootElement.ValueKind == JsonValueKind.String)
+ {
+ var valueAsString = Props.RootElement.GetString() ?? "";
+ if (TryParseDocument(valueAsString, out var doc) && doc is not null)
+ Props = doc;
+ else
+ return (false, $"Props must be an object or a valid stringified JSON, was: {Props.RootElement.GetRawText()}");
+ }
+
+ if (Props.RootElement.ValueKind != JsonValueKind.Object)
+ return (false, $"Props must be an object or a valid stringified JSON, was: {Props.RootElement.GetRawText()}");
+
+ foreach (var prop in Props.RootElement.EnumerateObject())
+ {
+ if (string.IsNullOrWhiteSpace(prop.Name))
+ return (false, "Property key must not be empty.");
+
+ if (prop.Name.Length > 40)
+ return (false, $"Property key '{prop.Name}' must be less than or equal to 40 characters. Props was: {Props.RootElement.GetRawText()}");
+ }
+ }
+
+ return (true, string.Empty);
+ }
+
+ private static bool TryParseDocument(string json, out JsonDocument? doc)
+ {
+ try
+ {
+ doc = JsonDocument.Parse(json);
+ return true;
+ }
+ catch
+ {
+ doc = null;
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Ingestion/EventsController.cs b/src/Features/Ingestion/EventsController.cs
new file mode 100755
index 0000000..a5e9176
--- /dev/null
+++ b/src/Features/Ingestion/EventsController.cs
@@ -0,0 +1,157 @@
+using Aptabase.Features.GeoIP;
+using Aptabase.Features.Ingestion.Buffer;
+using Microsoft.AspNetCore.Cors;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.RateLimiting;
+
+namespace Aptabase.Features.Ingestion;
+
+[ApiController]
+[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
+public class EventsController : Controller
+{
+ private readonly ILogger _logger;
+ private readonly IIngestionCache _cache;
+ private readonly IEventBuffer _buffer;
+ private readonly GeoIPClient _geoIP;
+
+ public EventsController(IIngestionCache cache,
+ IEventBuffer buffer,
+ GeoIPClient geoIP,
+ ILogger logger)
+ {
+ _cache = cache ?? throw new ArgumentNullException(nameof(cache));
+ _buffer = buffer ?? throw new ArgumentNullException(nameof(buffer));
+ _geoIP = geoIP ?? throw new ArgumentNullException(nameof(geoIP));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ [HttpPost("/api/v0/event")]
+ [EnableCors("AllowAny")]
+ [EnableRateLimiting("EventIngestion")]
+ public async Task Single(
+ [FromHeader(Name = "App-Key")] string? appKey,
+ [FromHeader(Name = "User-Agent")] string? userAgent,
+ [FromBody] EventBody body,
+ CancellationToken cancellationToken
+ )
+ {
+ appKey = appKey?.ToUpper() ?? "";
+
+ var (valid, validationMessage) = body.IsValid(_logger);
+ if (!valid)
+ {
+ _logger.LogWarning($"Dropping event from {appKey} because: {validationMessage}.");
+ return BadRequest(validationMessage);
+ }
+
+ var app = await _cache.FindByAppKey(appKey, cancellationToken);
+ if (string.IsNullOrEmpty(app.Id))
+ return AppNotFound(appKey);
+
+ if (app.IsLocked)
+ return BadRequest($"Owner account is locked.");
+
+ // We never expect the Web SDK to send the OS name, so it's safe to assume that if it's missing the event is coming from a browser
+ var isWeb = string.IsNullOrEmpty(body.SystemProps.OSName);
+
+ // For web events, we need to parse the user agent to get the OS name and version
+ if (isWeb && !string.IsNullOrEmpty(userAgent))
+ {
+ var (osName, osVersion) = UserAgentParser.ParseOperatingSystem(userAgent);
+ body.SystemProps.OSName = osName;
+ body.SystemProps.OSVersion = osVersion;
+
+ var (engineName, engineVersion) = UserAgentParser.ParseBrowser(userAgent);
+ body.SystemProps.EngineName = engineName;
+ body.SystemProps.EngineVersion = engineVersion;
+ }
+
+ // We can't rely on User-Agent header sent by the SDK for non-web events, so we fabricate one
+ // This can be removed when this issue is implemented: https://github.com/aptabase/aptabase/issues/23
+ if (!isWeb)
+ userAgent = $"{body.SystemProps.OSName}/{body.SystemProps.OSVersion} {body.SystemProps.EngineName}/{body.SystemProps.EngineVersion} {body.SystemProps.Locale}";
+
+ var clientIp = HttpContext.ResolveClientIpAddress();
+ var location = _geoIP.GetClientLocation(HttpContext);
+ var trackingEvent = NewTrackingEvent(app.Id, location.CountryCode, location.RegionName, clientIp, userAgent ?? "", body);
+ _buffer.Add(ref trackingEvent);
+
+ return Ok(new { });
+ }
+
+ [HttpPost("/api/v0/events")]
+ [EnableRateLimiting("EventIngestion")]
+ public async Task Multiple(
+ [FromHeader(Name = "App-Key")] string? appKey,
+ [FromHeader(Name = "User-Agent")] string? userAgent,
+ [FromBody] EventBody[] events,
+ CancellationToken cancellationToken
+ )
+ {
+ appKey = appKey?.ToUpper() ?? "";
+
+ if (events.Length > 25)
+ return BadRequest($"Too many events ({events.Length}) in a single request. Maximum is 25.");
+
+ var validEvents = events.Where(e => {
+ var (valid, validationMessage) = e.IsValid(_logger);
+ if (!valid)
+ _logger.LogWarning("Dropping event from {AppKey}. {ValidationMessage}", appKey, validationMessage);
+ return valid;
+ }).ToArray();
+
+ if (!validEvents.Any())
+ return Ok(new { });
+
+ var app = await _cache.FindByAppKey(appKey, cancellationToken);
+ if (string.IsNullOrEmpty(app.Id))
+ return AppNotFound(appKey);
+
+ if (app.IsLocked)
+ return BadRequest($"Owner account is locked.");
+
+ var clientIp = HttpContext.ResolveClientIpAddress();
+ var location = _geoIP.GetClientLocation(HttpContext);
+ var trackingEvents = validEvents.Select(e => NewTrackingEvent(app.Id, location.CountryCode, location.RegionName, clientIp, userAgent ?? "", e));
+
+ _buffer.AddRange(ref trackingEvents);
+
+ return Ok(new { });
+ }
+
+ private IActionResult AppNotFound(string appKey)
+ {
+ _logger.LogWarning("Appplication not found with given app key: {AppKey}", appKey);
+ return NotFound($"Appplication not found with given app key: {appKey}");
+ }
+
+ private static TrackingEvent NewTrackingEvent(string appId, string countryCode, string regionName, string clientIp, string userAgent, EventBody body)
+ {
+ var (stringProps, numericProps) = body.SplitProps();
+ return new TrackingEvent
+ {
+ ClientIpAddress = clientIp,
+ UserAgent = userAgent,
+
+ AppId = appId,
+ EventName = body.EventName,
+ Timestamp = body.Timestamp.ToUniversalTime(),
+ SessionId = body.SessionId?.ToString() ?? "",
+ OSName = body.SystemProps.OSName ?? "",
+ OSVersion = body.SystemProps.OSVersion ?? "",
+ DeviceModel = body.SystemProps.DeviceModel ?? "",
+ Locale = body.SystemProps.Locale ?? "",
+ AppVersion = body.SystemProps.AppVersion ?? "",
+ EngineName = body.SystemProps.EngineName ?? "",
+ EngineVersion = body.SystemProps.EngineVersion ?? "",
+ AppBuildNumber = body.SystemProps.AppBuildNumber ?? "",
+ SdkVersion = body.SystemProps.SdkVersion ?? "",
+ CountryCode = countryCode,
+ RegionName = regionName,
+ StringProps = stringProps.ToJsonString(),
+ NumericProps = numericProps.ToJsonString(),
+ IsDebug = body.SystemProps.IsDebug,
+ };
+ }
+}
diff --git a/src/Features/Ingestion/IngestionCache.cs b/src/Features/Ingestion/IngestionCache.cs
new file mode 100755
index 0000000..243ae5c
--- /dev/null
+++ b/src/Features/Ingestion/IngestionCache.cs
@@ -0,0 +1,67 @@
+using Aptabase.Features.Apps;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace Aptabase.Features.Ingestion;
+
+public class CachedApplication
+{
+ public string Id { get; private set; }
+ public bool IsLocked { get; private set; }
+
+ public CachedApplication()
+ {
+ Id = string.Empty;
+ }
+
+ public CachedApplication(Application app)
+ {
+ Id = app.Id;
+ IsLocked = app.LockReason.HasValue;
+ }
+
+ public static CachedApplication Empty => new CachedApplication();
+}
+
+public interface IIngestionCache
+{
+ Task FindByAppKey(string appKey, CancellationToken cancellationToken);
+}
+
+public class IngestionCache : IIngestionCache
+{
+ private readonly IMemoryCache _cache;
+ private readonly IAppQueries _appQueries;
+
+ private readonly TimeSpan SuccessCacheDuration = TimeSpan.FromMinutes(30);
+ private readonly TimeSpan FailureCacheDuration = TimeSpan.FromMinutes(5);
+
+ public IngestionCache(IMemoryCache cache, IAppQueries appQueries)
+ {
+ _cache = cache ?? throw new ArgumentNullException(nameof(cache));
+ _appQueries = appQueries ?? throw new ArgumentNullException(nameof(appQueries));
+ }
+
+ public async Task FindByAppKey(string appKey, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(appKey))
+ return CachedApplication.Empty;
+
+ var cacheKey = $"APP-KEY-STATUS-{appKey}";
+ if (_cache.TryGetValue(cacheKey, out CachedApplication? cachedApp) && cachedApp is not null)
+ return cachedApp;
+
+ var app = await _appQueries.GetActiveAppByAppKey(appKey, cancellationToken);
+ if (app is null)
+ {
+ _cache.Set(cacheKey, CachedApplication.Empty, FailureCacheDuration);
+ return CachedApplication.Empty;
+ }
+
+ if (!app.HasEvents)
+ await _appQueries.MaskAsOnboarded(app.Id, cancellationToken);
+
+ cachedApp = new CachedApplication(app);
+ _cache.Set(cacheKey, cachedApp, SuccessCacheDuration);
+ return cachedApp;
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Ingestion/LocaleFormatter.cs b/src/Features/Ingestion/LocaleFormatter.cs
new file mode 100755
index 0000000..8d64dec
--- /dev/null
+++ b/src/Features/Ingestion/LocaleFormatter.cs
@@ -0,0 +1,38 @@
+namespace Aptabase.Features.Ingestion;
+
+public static class LocaleFormatter
+{
+
+ // List of special locales and the expected format
+ private static Dictionary SpecialCases = new()
+ {
+ { "en-001", "en" },
+ { "en-150", "en" },
+ { "en-us-posix", "en" },
+ };
+
+ // Validates and formats a locale string
+ // Return a lowercase locale string
+ // Return empty string for empty or the special 'C' locale
+ // Returns null for invalid/unsupported locales
+ public static string? FormatLocale(string? locale)
+ {
+ if (string.IsNullOrEmpty(locale) || locale == "C")
+ return "";
+
+ // Some systems may use '_' instead of '-'
+ // Lowercase it to be consistent
+ locale = locale.Replace("_", "-").ToLower();
+
+ if (SpecialCases.ContainsKey(locale))
+ return SpecialCases[locale];
+
+ return locale.Length switch
+ {
+ 2 => locale,
+ (>= 5) and (<= 7) => locale[2] == '-' ? locale : null,
+ 10 => locale[2] == '-' && locale[7] == '-' ? locale : null,
+ _ => null
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Ingestion/UserAgentParser.cs b/src/Features/Ingestion/UserAgentParser.cs
new file mode 100755
index 0000000..4047f81
--- /dev/null
+++ b/src/Features/Ingestion/UserAgentParser.cs
@@ -0,0 +1,88 @@
+using System.Text.RegularExpressions;
+
+namespace Aptabase.Features.Ingestion;
+
+public static class UserAgentParser
+{
+ private static Dictionary osKeys = new Dictionary
+ {
+ { "ipad", "iPadOS" },
+ { "iphone", "iOS" },
+ { "ios", "iOS" },
+
+ { "windows nt", "Windows" },
+
+ { "mac os x", "macOS" },
+ { "macos", "macOS" },
+ { "macintosh", "macOS" },
+
+ { "android", "Android" },
+
+ { "cros", "Chrome OS" },
+ { "ubuntu", "Ubuntu" },
+ { "fedora", "Fedora" },
+ { "arch linux", "Arch Linux" },
+ { "archlinux", "Arch Linux" },
+ { "linux", "Linux" }
+ };
+
+ public static (string, string) ParseOperatingSystem(string userAgent)
+ {
+ var lcUserAgent = userAgent.ToLowerInvariant();
+
+ foreach (var (key, value) in osKeys)
+ {
+ if (lcUserAgent.Contains(key))
+ return (value, "");
+ }
+
+ return ("", "");
+ }
+
+
+ private static Dictionary browserKeys = new Dictionary
+ {
+ { "Edg", "Edge" },
+ { "Firefox", "Firefox" },
+ { "OPiOS", "Opera" },
+ { "OPR", "Opera" },
+ { "YaBrowser", "Yandex Browser" }
+ };
+
+ private static Regex browserRegex = new Regex(@"\((?.*?)\)(\s|$)|(?.*?)\/(?.*?)(\s|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ public static (string, string) ParseBrowser(string userAgent)
+ {
+ var lcUserAgent = userAgent.ToLowerInvariant();
+ var matches = browserRegex.Matches(userAgent);
+
+ var chromeVersion = "";
+ foreach (Match match in matches)
+ {
+ var name = match.Groups["name"].Value;
+ var version = match.Groups["version"].Value;
+
+ foreach (var (key, prettyName) in browserKeys)
+ {
+ if (name == key)
+ return (prettyName, version);
+ }
+
+ if (name == "Chrome")
+ chromeVersion = version;
+
+ if (name == "Safari")
+ {
+ // It's not Safari unless it has a "Version" group
+ var safariVersion = matches.FirstOrDefault(x => x.Groups["name"].Value == "Version")?.Groups["version"].Value ?? "";
+ if (!string.IsNullOrEmpty(safariVersion))
+ return ("Safari", safariVersion);
+ }
+ }
+
+ // if we didn't find a browser, but we found a chrome version, we assume it's chrome
+ if (!string.IsNullOrEmpty(chromeVersion))
+ return ("Chrome", chromeVersion);
+
+ return ("", "");
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Notification/IEmailClient.cs b/src/Features/Notification/IEmailClient.cs
new file mode 100755
index 0000000..af6ed8a
--- /dev/null
+++ b/src/Features/Notification/IEmailClient.cs
@@ -0,0 +1,6 @@
+namespace Aptabase.Features.Notification;
+
+public interface IEmailClient
+{
+ Task SendEmailAsync(string to, string subject, string templateName, Dictionary? properties, CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/src/Features/Notification/LoggerEmailClient.cs b/src/Features/Notification/LoggerEmailClient.cs
new file mode 100755
index 0000000..1a8eea0
--- /dev/null
+++ b/src/Features/Notification/LoggerEmailClient.cs
@@ -0,0 +1,18 @@
+namespace Aptabase.Features.Notification;
+
+public class LoggerEmailClient : IEmailClient
+{
+ private readonly TemplateEngine _engine = new();
+ private readonly ILogger _logger;
+
+ public LoggerEmailClient(ILogger logger)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task SendEmailAsync(string to, string subject, string templateName, Dictionary? properties, CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Sending email to '{to}' with template '{templateName}' and '{properties}'", to, templateName, properties);
+ await Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Notification/MailCatcherSmtpClient.cs b/src/Features/Notification/MailCatcherSmtpClient.cs
new file mode 100755
index 0000000..df33b70
--- /dev/null
+++ b/src/Features/Notification/MailCatcherSmtpClient.cs
@@ -0,0 +1,27 @@
+using System.Net.Mail;
+
+namespace Aptabase.Features.Notification;
+
+public class MailCatcherSmtpClient : IEmailClient
+{
+ private readonly TemplateEngine _engine = new();
+ private readonly SmtpClient _client;
+
+ public MailCatcherSmtpClient(string host, int port)
+ {
+ _client = new SmtpClient(host, port);
+ }
+
+ public async Task SendEmailAsync(string to, string subject, string templateName, Dictionary? properties, CancellationToken cancellationToken)
+ {
+ var body = await _engine.Render(templateName, subject, properties);
+
+ using var message = new MailMessage("hi@aptabase.com", to)
+ {
+ Subject = subject,
+ Body = body
+ };
+
+ await _client.SendMailAsync(message, cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Notification/NotificationExtensions.cs b/src/Features/Notification/NotificationExtensions.cs
new file mode 100755
index 0000000..5b1a462
--- /dev/null
+++ b/src/Features/Notification/NotificationExtensions.cs
@@ -0,0 +1,32 @@
+namespace Aptabase.Features.Notification;
+
+public static class NotificationExtensions
+{
+ public static void AddEmailClient(this IServiceCollection services, EnvSettings env)
+ {
+ if (env.IsManagedCloud)
+ {
+ services.AddSingleton();
+ return;
+ }
+
+ if (!string.IsNullOrEmpty(env.MailCatcherConnectionString))
+ {
+ services.AddSingleton(sp =>
+ {
+ var smtpUri = new Uri(env.MailCatcherConnectionString);
+
+ return new MailCatcherSmtpClient(smtpUri.Host, smtpUri.Port);
+ });
+ return;
+ }
+
+ if (!string.IsNullOrEmpty(env.SmtpHost))
+ {
+ services.AddSingleton();
+ return;
+ }
+
+ services.AddSingleton();
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Notification/SESEmailClient.cs b/src/Features/Notification/SESEmailClient.cs
new file mode 100755
index 0000000..4d5577a
--- /dev/null
+++ b/src/Features/Notification/SESEmailClient.cs
@@ -0,0 +1,62 @@
+using Amazon.Runtime.CredentialManagement;
+using Amazon.SimpleEmail;
+using Amazon.SimpleEmail.Model;
+
+namespace Aptabase.Features.Notification;
+
+public class SESEmailClient : IEmailClient
+{
+ private readonly IAmazonSimpleEmailService _ses;
+ private readonly EnvSettings _env;
+ private readonly TemplateEngine _engine = new();
+
+ public SESEmailClient(EnvSettings env)
+ {
+ _env = env ?? throw new ArgumentNullException(nameof(env));
+ _ses = CreateClient();
+ }
+
+ public async Task SendEmailAsync(string to, string subject, string templateName, Dictionary? properties, CancellationToken cancellationToken)
+ {
+ var body = await _engine.Render(templateName, subject, properties);
+ var request = NewRequest(to, subject, body);
+ await _ses.SendEmailAsync(request, cancellationToken);
+ }
+
+ private AmazonSimpleEmailServiceClient CreateClient()
+ {
+ if (_env.IsProduction)
+ return new AmazonSimpleEmailServiceClient();
+
+ var chain = new CredentialProfileStoreChain();
+ if (!chain.TryGetAWSCredentials("aptabase", out var credentials))
+ throw new Exception("Failed to find the aptabase profile");
+
+ return new AmazonSimpleEmailServiceClient(credentials, Amazon.RegionEndpoint.USEast1);
+ }
+
+ private static SendEmailRequest NewRequest(string to, string subject, string body)
+ {
+ return new SendEmailRequest
+ {
+ Source = "Aptabase ",
+ Destination = new Destination
+ {
+ ToAddresses =
+ new List { to }
+ },
+ Message = new Message
+ {
+ Subject = new Content(subject),
+ Body = new Body
+ {
+ Html = new Content
+ {
+ Charset = "UTF-8",
+ Data = body
+ },
+ }
+ },
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Notification/SmtpEmailClient.cs b/src/Features/Notification/SmtpEmailClient.cs
new file mode 100755
index 0000000..12f6a08
--- /dev/null
+++ b/src/Features/Notification/SmtpEmailClient.cs
@@ -0,0 +1,35 @@
+using MimeKit;
+using MailKit.Net.Smtp;
+
+namespace Aptabase.Features.Notification;
+
+public class SmtpEmailClient : IEmailClient
+{
+ private readonly EnvSettings _env;
+ private readonly TemplateEngine _engine = new();
+
+ public SmtpEmailClient(EnvSettings env)
+ {
+ _env = env ?? throw new ArgumentNullException(nameof(env));
+ }
+
+ public async Task SendEmailAsync(string to, string subject, string templateName, Dictionary? properties, CancellationToken cancellationToken)
+ {
+ using var smtp = new SmtpClient();
+ await smtp.ConnectAsync(_env.SmtpHost, _env.SmtpPort, IsTLSPort(_env.SmtpPort), cancellationToken);
+
+ if (!string.IsNullOrEmpty(_env.SmtpUsername))
+ await smtp.AuthenticateAsync(_env.SmtpUsername, _env.SmtpPassword, cancellationToken);
+
+ var body = await _engine.Render(templateName, subject, properties);
+ var msg = new MimeMessage();
+ msg.From.Add(new MailboxAddress("", _env.SmtpFromAddress));
+ msg.To.Add(new MailboxAddress("", to));
+ msg.Subject = subject;
+ msg.Body = new TextPart("html") { Text = body };
+ await smtp.SendAsync(msg, cancellationToken);
+ await smtp.DisconnectAsync(true, cancellationToken);
+ }
+
+ private static bool IsTLSPort(int port) => port == 587 || port == 465;
+}
\ No newline at end of file
diff --git a/src/Features/Notification/TemplateEngine.cs b/src/Features/Notification/TemplateEngine.cs
new file mode 100755
index 0000000..c60b89a
--- /dev/null
+++ b/src/Features/Notification/TemplateEngine.cs
@@ -0,0 +1,40 @@
+using System.Reflection;
+using System.Text;
+using System.Web;
+
+namespace Aptabase.Features.Notification;
+
+public class TemplateEngine
+{
+ private readonly Assembly _assembly = Assembly.GetExecutingAssembly() ?? throw new Exception("Failed to find the entry assembly");
+ private readonly Dictionary _templates = new();
+
+ public async Task Render(string name, string subject, Dictionary? properties)
+ {
+ var baseTemplate = await GetTemplate("Base");
+ var emailTemplate = await GetTemplate(name);
+
+ if (properties is not null)
+ {
+ foreach (var (key, value) in properties)
+ emailTemplate = emailTemplate.Replace($"##{key.ToUpper()}##", HttpUtility.HtmlEncode(value));
+ }
+
+ return baseTemplate.Replace("##SUBJECT##", subject).Replace("##BODY##", emailTemplate);
+ }
+
+ public async Task GetTemplate(string name)
+ {
+ if (_templates.ContainsKey(name))
+ return _templates[name];
+
+ var resourceStream = _assembly.GetManifestResourceStream($"Aptabase.assets.Templates.{name}.html");
+ if (resourceStream == null)
+ throw new Exception($"Failed to find the embedded resource named {name}");
+
+ var reader = new StreamReader(resourceStream, Encoding.UTF8);
+ var content = await reader.ReadToEndAsync();
+ _templates.Add(name, content);
+ return content;
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Privacy/DailyUserHasher.cs b/src/Features/Privacy/DailyUserHasher.cs
new file mode 100755
index 0000000..16ce50a
--- /dev/null
+++ b/src/Features/Privacy/DailyUserHasher.cs
@@ -0,0 +1,70 @@
+using Aptabase.Data;
+using System.Text;
+using System.Security.Cryptography;
+using Microsoft.Extensions.Caching.Memory;
+using Dapper;
+using FastHashes;
+
+namespace Aptabase.Features.Privacy;
+
+public interface IUserHasher
+{
+ Task CalculateHash(DateTime timestamp, string appId, string sessionId, string clientIP, string userAgent);
+}
+
+public class DailyUserHasher : IUserHasher
+{
+ private readonly IMemoryCache _cache;
+ private readonly IDbContext _db;
+
+ public DailyUserHasher(IMemoryCache cache, IDbContext db)
+ {
+ _db = db ?? throw new ArgumentNullException(nameof(db));
+ _cache = cache ?? throw new ArgumentNullException(nameof(cache));
+ }
+
+ public async Task CalculateHash(DateTime timestamp, string appId, string sessionId, string clientIP, string userAgent)
+ {
+ var cacheKey = $"USERID-{appId}-{sessionId}";
+
+ // If we already have a cached user ID for this session, return it immediately
+ // This avoid issues with the user ID changing in the middle of a session because of an IP change
+ if (_cache.TryGetValue(cacheKey, out string? userId) && !string.IsNullOrEmpty(userId))
+ return userId;
+
+ var salt = await GetSaltFor(timestamp.Date.ToString("yyyy-MM-dd"), appId);
+ var bytes = Encoding.UTF8.GetBytes($"{clientIP}|${userAgent}");
+ var hash = ComputeHash(salt, bytes);
+ userId = Convert.ToHexString(hash);
+
+ _cache.Set(cacheKey, userId, TimeSpan.FromHours(48));
+ return userId;
+ }
+
+ private static byte[] ComputeHash(Span salt, byte[] bytes)
+ {
+ var key1 = BitConverter.ToUInt64(salt[..8]);
+ var key2 = BitConverter.ToUInt64(salt[0..]);
+ var hasher = new SipHash(SipHashVariant.V24, key1, key2);
+ return hasher.ComputeHash(bytes);
+ }
+
+ private async Task GetSaltFor(string date, string appId)
+ {
+ var cacheKey = $"DAILY-SALT-{appId}-{date}";
+ if (_cache.TryGetValue(cacheKey, out byte[]? cachedSalt) && cachedSalt != null)
+ return cachedSalt;
+
+ var storedSalt = await ReadOrCreateSalt(date, appId);
+ _cache.Set(cacheKey, storedSalt, TimeSpan.FromDays(2));
+ return storedSalt;
+ }
+
+ private async Task ReadOrCreateSalt(string date, string appId)
+ {
+ var newSalt = RandomNumberGenerator.GetBytes(16);
+ await _db.Connection.ExecuteAsync($"INSERT INTO app_salts (app_id, date, salt) VALUES (@appId, @date, @newSalt) ON CONFLICT DO NOTHING", new { appId, date, newSalt });
+ var bytes = await _db.Connection.ExecuteScalarAsync($"SELECT salt FROM app_salts WHERE app_id = @appId AND date = @date", new { appId, date });
+ return bytes ?? newSalt;
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Privacy/PrivacyQueries.cs b/src/Features/Privacy/PrivacyQueries.cs
new file mode 100755
index 0000000..2485778
--- /dev/null
+++ b/src/Features/Privacy/PrivacyQueries.cs
@@ -0,0 +1,29 @@
+using Aptabase.Data;
+using Dapper;
+
+namespace Aptabase.Features.Privacy;
+
+public interface IPrivacyQueries
+{
+ Task PurgeOldSalts(CancellationToken cancellationToken);
+}
+
+public class PrivacyQueries : IPrivacyQueries
+{
+ private readonly IDbContext _db;
+
+ public PrivacyQueries(IDbContext db)
+ {
+ _db = db ?? throw new ArgumentNullException(nameof(db));
+ }
+
+ public async Task PurgeOldSalts(CancellationToken cancellationToken)
+ {
+ var cmd = new CommandDefinition(
+ "DELETE FROM app_salts WHERE TO_DATE(date, 'YYYY/MM/DD') <= CURRENT_DATE - INTERVAL '2' DAY",
+ cancellationToken: cancellationToken
+ );
+
+ return await _db.Connection.ExecuteAsync(cmd);
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Privacy/PurgeDailySaltsCronJob.cs b/src/Features/Privacy/PurgeDailySaltsCronJob.cs
new file mode 100755
index 0000000..58c2fbf
--- /dev/null
+++ b/src/Features/Privacy/PurgeDailySaltsCronJob.cs
@@ -0,0 +1,43 @@
+using Sgbj.Cron;
+
+namespace Aptabase.Features.Privacy;
+
+public class PurgeDailySaltsCronJob : BackgroundService
+{
+ private readonly ILogger _logger;
+ private readonly IPrivacyQueries _privacyQueries;
+
+ public PurgeDailySaltsCronJob(IPrivacyQueries privacyQueries, ILogger logger)
+ {
+ _privacyQueries = privacyQueries ?? throw new ArgumentNullException(nameof(privacyQueries));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ _logger.LogInformation("PurgeDailySaltsCronJob is starting.");
+
+ using var timer = new CronTimer("0 0 * * *", TimeZoneInfo.Utc);
+
+ while (await timer.WaitForNextTickAsync(cancellationToken))
+ {
+ _logger.LogInformation("Purging daily salts");
+ var rows = await _privacyQueries.PurgeOldSalts(cancellationToken);
+ _logger.LogInformation("Deleted {rows} rows", rows);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.LogInformation("PurgeDailySaltsCronJob stopped.");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "PurgeDailySaltsCronJob crashed.");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Stats/ClickHouseQueryClient.cs b/src/Features/Stats/ClickHouseQueryClient.cs
new file mode 100755
index 0000000..edfd6a2
--- /dev/null
+++ b/src/Features/Stats/ClickHouseQueryClient.cs
@@ -0,0 +1,65 @@
+using Dapper;
+using Scriban;
+using ClickHouse.Client.ADO;
+using System.Collections.Concurrent;
+
+namespace Aptabase.Features.Stats;
+
+public class ClickHouseQueryClient : IQueryClient
+{
+ private readonly ClickHouseConnection _conn;
+ private readonly EnvSettings _env;
+
+ public ClickHouseQueryClient(ClickHouseConnection conn, EnvSettings env)
+ {
+ _env = env ?? throw new ArgumentNullException(nameof(env));
+ _conn = conn ?? throw new ArgumentNullException(nameof(conn));
+ }
+
+ public async Task> NamedQueryAsync(string name, object args, CancellationToken cancellationToken)
+ {
+ var dict = args.GetType().GetProperties().ToDictionary(p => p.Name, p => FormatArg(p.GetValue(args, null)));
+ var template = await ReadNamedQuery(name);
+ var query = await template.RenderAsync(dict);
+ return await _conn.QueryAsync(query, cancellationToken);
+ }
+
+ public async Task NamedQuerySingleAsync(string name, object args, CancellationToken cancellationToken) where T : new()
+ {
+ var rows = await NamedQueryAsync(name, args, cancellationToken);
+ return rows.FirstOrDefault() ?? new T();
+ }
+
+ public async Task StreamResponseAsync(string query, CancellationToken cancellationToken)
+ {
+ using var command = _conn.CreateCommand();
+ command.CommandText = query;
+ var result = await command.ExecuteRawResultAsync(cancellationToken);
+ return await result.ReadAsStreamAsync();
+ }
+
+ private readonly ConcurrentDictionary _namedQueries = new();
+ private async Task ReadNamedQuery(string name)
+ {
+ if (_namedQueries.ContainsKey(name))
+ return _namedQueries[name];
+
+ var pathToQuery = Path.Combine(_env.EtcDirectoryPath, "clickhouse", "queries", $"{name}.liquid");
+ var content = await File.ReadAllTextAsync(pathToQuery);
+ var template = Template.ParseLiquid(content);
+
+ _namedQueries[name] = template;
+ return template;
+ }
+
+ private string? FormatArg(object? value)
+ {
+ return value switch
+ {
+ string[] s => string.Join("','", s),
+ DateTime d => d.ToString("yyyy-MM-dd HH:mm:ss"),
+ null => null,
+ _ => $"{value}",
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Stats/HasReadAccessToAppFilter.cs b/src/Features/Stats/HasReadAccessToAppFilter.cs
new file mode 100755
index 0000000..55965f1
--- /dev/null
+++ b/src/Features/Stats/HasReadAccessToAppFilter.cs
@@ -0,0 +1,25 @@
+
+using Aptabase.Data;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+
+namespace Aptabase.Features.Stats;
+
+public class HasReadAccessToApp : ActionFilterAttribute
+{
+ public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+ {
+ var db = context.HttpContext.RequestServices.GetService() ?? throw new InvalidOperationException("Could not get database context.");
+ var user = context.HttpContext.GetCurrentUserIdentity();
+ var appId = context.HttpContext.Request.Query["AppId"].ToString();
+
+ var hasAccess = await db.HasReadAccessToApp(appId, user, context.HttpContext.RequestAborted);
+ if (!hasAccess)
+ {
+ context.Result = new StatusCodeResult(403);
+ return;
+ }
+
+ await next();
+ }
+}
\ No newline at end of file
diff --git a/src/Features/Stats/IQueryClient.cs b/src/Features/Stats/IQueryClient.cs
new file mode 100755
index 0000000..3c150ab
--- /dev/null
+++ b/src/Features/Stats/IQueryClient.cs
@@ -0,0 +1,8 @@
+namespace Aptabase.Features.Stats;
+
+public interface IQueryClient
+{
+ Task> NamedQueryAsync(string name, object args, CancellationToken cancellationToken);
+ Task NamedQuerySingleAsync(string name, object args, CancellationToken cancellationToken) where T : new();
+ Task StreamResponseAsync(string query, CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/src/Features/Stats/StatsController.cs b/src/Features/Stats/StatsController.cs
new file mode 100755
index 0000000..de85557
--- /dev/null
+++ b/src/Features/Stats/StatsController.cs
@@ -0,0 +1,417 @@
+using Aptabase.Features.Authentication;
+using Aptabase.Features.GeoIP;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.RateLimiting;
+
+namespace Aptabase.Features.Stats;
+
+public record PeriodicStatsRow
+{
+ public DateTime Period { get; set; }
+ public double Users { get; set; } = 0;
+ public int Sessions { get; set; } = 0;
+ public int Events { get; set; } = 0;
+}
+
+public record KeyMetrics
+{
+ public KeyMetricsRow Current { get; set; } = new KeyMetricsRow();
+ public KeyMetricsRow? Previous { get; set; }
+
+ public KeyMetrics()
+ {
+ }
+
+ public KeyMetrics(KeyMetricsRow current)
+ {
+ Current = current;
+ }
+
+ public KeyMetrics(KeyMetricsRow current, KeyMetricsRow previous)
+ {
+ Current = current;
+ Previous = previous;
+ }
+}
+
+public record KeyMetricsRow
+{
+ public double DailyUsers { get; set; } = 0;
+ public int Sessions { get; set; } = 0;
+ public int Events { get; set; } = 0;
+ public double DurationSeconds { get; set; } = 0;
+}
+
+public record TopNItem
+{
+ public string Name { get; set; } = "";
+ public double Value { get; set; }
+}
+
+public record EventPropKeys
+{
+ public string[] StringKeys { get; set; } = Array.Empty();
+ public string[] NumericKeys { get; set; } = Array.Empty();
+}
+
+public record EventPropsItem
+{
+ public string StringKey { get; set; } = "";
+ public string StringValue { get; set; } = "";
+ public string NumericKey { get; set; } = "";
+ public int Events { get; set; }
+ public decimal Median { get; set; }
+ public decimal Min { get; set; }
+ public decimal Max { get; set; }
+ public decimal Sum { get; set; }
+}
+
+public record LiveGeoDataPoint
+{
+ public string CountryCode { get; set; } = "";
+ public string RegionName { get; set; } = "";
+ public ulong Users { get; set; }
+ public double Latitude { get; set; }
+ public double Longitude { get; set; }
+}
+
+public record LiveRecentSession
+{
+ public string Id { get; set; } = "";
+ public DateTime StartedAt { get; set; }
+ public ulong Duration { get; set; }
+ public ulong EventsCount { get; set; }
+ public string AppVersion { get; set; } = "";
+ public string CountryCode { get; set; } = "";
+ public string RegionName { get; set; } = "";
+ public string OsName { get; set; } = "";
+ public string OsVersion { get; set; } = "";
+ public string DeviceModel { get; set; } = "";
+}
+
+public record SessionTimeline
+{
+ public string Id { get; set; } = "";
+ public DateTime StartedAt { get; set; }
+ public ulong Duration { get; set; }
+ public ulong EventsCount { get; set; }
+ public string AppVersion { get; set; } = "";
+ public string CountryCode { get; set; } = "";
+ public string RegionName { get; set; } = "";
+ public string OsName { get; set; } = "";
+ public string OsVersion { get; set; } = "";
+ public string[] EventsName { get; set; } = Array.Empty();
+ public DateTime[] EventsTimestamp { get; set; } = Array.Empty();
+ public string[] EventsStringProps { get; set; } = Array.Empty();
+ public string[] EventsNumericProps { get; set; } = Array.Empty();
+}
+
+public enum TopNValue
+{
+ UniqueSessions,
+ TotalEvents
+}
+
+public enum GranularityEnum
+{
+ Hour,
+ Day,
+ Month
+}
+
+public record QueryArgs
+{
+ public string AppId { get; set; } = "";
+ public string? SessionId { get; set; } = "";
+ public DateTime? DateFrom { get; set; }
+ public DateTime? DateTo { get; set; }
+ public GranularityEnum? Granularity { get; set; }
+ public string? CountryCode { get; set; }
+ public string? OsName { get; set; }
+ public string? DeviceModel { get; set; }
+ public string? EventName { get; set; }
+ public string? AppVersion { get; set; }
+
+ public QueryArgs CloneToPreviousInterval()
+ {
+ if (!DateFrom.HasValue || !DateTo.HasValue)
+ {
+ return this;
+ }
+
+ var previousEnd = DateFrom.Value;
+ // Go back 1 second so that when relativeTo is 00:00:00 we start with the previous day
+ // Happens when looking for previous period and "period" is "month" or "last-month"
+ if (previousEnd.TimeOfDay == TimeSpan.Zero)
+ previousEnd = previousEnd.AddSeconds(-1);
+ var previousStart =
+ DateFrom.Value.Subtract(
+ DateTo.Value.Subtract((DateFrom.Value))); //start - (end - start)
+
+ return new QueryArgs
+ {
+ AppId = AppId,
+ SessionId = SessionId,
+ DateFrom = previousStart,
+ DateTo = previousEnd,
+ Granularity = Granularity,
+ CountryCode = CountryCode,
+ OsName = OsName,
+ EventName = EventName,
+ AppVersion = AppVersion,
+ };
+ }
+}
+
+public class QueryParams
+{
+ public string BuildMode { get; set; } = "";
+ public string AppId { get; set; } = "";
+ public string? SessionId { get; set; } = "";
+ public DateTime? StartDate { get; set; }
+ public DateTime? EndDate { get; set; }
+ public GranularityEnum? Granularity { get; set; }
+ public string? CountryCode { get; set; }
+ public string? OsName { get; set; }
+ public string? EventName { get; set; }
+ public string? AppVersion { get; set; }
+ public string? DeviceModel { get; set; }
+
+ public QueryArgs Parse()
+ {
+ var appId = BuildMode.ToLower() switch
+ {
+ "debug" => $"{AppId}_DEBUG",
+ _ => AppId,
+ };
+
+ return new QueryArgs
+ {
+ AppId = appId,
+ SessionId = SessionId,
+ DateFrom = StartDate,
+ DateTo = EndDate,
+ Granularity = Granularity,
+ CountryCode = CountryCode,
+ OsName = OsName,
+ DeviceModel = DeviceModel,
+ EventName = EventName,
+ AppVersion = AppVersion,
+ };
+ }
+}
+
+[ApiController, IsAuthenticated, HasReadAccessToApp]
+[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
+[EnableRateLimiting("Stats")]
+public class StatsController : Controller
+{
+ private readonly IQueryClient _queryClient;
+ private readonly GeoIPClient _geodb;
+
+ public StatsController(IQueryClient queryClient, GeoIPClient geodb)
+ {
+ _geodb = geodb ?? throw new ArgumentNullException(nameof(geodb));
+ _queryClient = queryClient ?? throw new ArgumentNullException(nameof(queryClient));
+ }
+
+ [HttpGet("/api/_stats/top-countries")]
+ public async Task TopCountries([FromQuery] QueryParams body, CancellationToken cancellationToken)
+ {
+ return await TopN("country_code", body.Granularity ?? GranularityEnum.Hour, TopNValue.UniqueSessions, body, cancellationToken);
+ }
+
+ [HttpGet("/api/_stats/top-osversions")]
+ public async Task TopOSVersions([FromQuery] QueryParams body, CancellationToken cancellationToken)
+ {
+ return await TopN("os_version", body.Granularity ?? GranularityEnum.Hour, TopNValue.UniqueSessions, body, cancellationToken);
+ }
+
+ [HttpGet("/api/_stats/top-devices")]
+ public async Task TopDevices([FromQuery] QueryParams body, CancellationToken cancellationToken)
+ {
+ return await TopN("device_model", body.Granularity ?? GranularityEnum.Hour, TopNValue.UniqueSessions, body, cancellationToken);
+ }
+
+ [HttpGet("/api/_stats/top-operatingsystems")]
+ public async Task TopOperatingSystems([FromQuery] QueryParams body, CancellationToken cancellationToken)
+ {
+ return await TopN("os_name", body.Granularity ?? GranularityEnum.Hour, TopNValue.UniqueSessions, body, cancellationToken);
+ }
+
+ [HttpGet("/api/_stats/top-regions")]
+ public async Task TopRegions([FromQuery] QueryParams body, CancellationToken cancellationToken)
+ {
+ return await TopN("region_name", body.Granularity ?? GranularityEnum.Hour, TopNValue.UniqueSessions, body, cancellationToken);
+ }
+
+ [HttpGet("/api/_stats/top-events")]
+ public async Task TopEvents([FromQuery] QueryParams body, CancellationToken cancellationToken)
+ {
+ return await TopN("event_name", body.Granularity ?? GranularityEnum.Hour, TopNValue.TotalEvents, body, cancellationToken);
+ }
+
+ [HttpGet("/api/_stats/top-appversions")]
+ public async Task TopAppVersions([FromQuery] QueryParams body, CancellationToken cancellationToken)
+ {
+ return await TopN("app_version", body.Granularity ?? GranularityEnum.Hour, TopNValue.UniqueSessions, body, cancellationToken);
+ }
+
+ [HttpGet("/api/_stats/top-appbuildnumbers")]
+ public async Task TopAppBuildNumbers([FromQuery] QueryParams body, CancellationToken cancellationToken)
+ {
+ return await TopN("app_build_number", body.Granularity ?? GranularityEnum.Hour, TopNValue.UniqueSessions, body, cancellationToken);
+ }
+
+ [HttpGet("/api/_stats/metrics")]
+ public async Task KeyMetrics([FromQuery] QueryParams body, CancellationToken cancellationToken)
+ {
+ var currentQuery = body.Parse();
+ var current = GetKeyMetrics(currentQuery, cancellationToken);
+
+ if (!currentQuery.DateFrom.HasValue || !currentQuery.DateTo.HasValue) {
+ return Ok(new KeyMetrics(await current));
+ }
+
+ var previousQuery = currentQuery.CloneToPreviousInterval();
+ var previous = await GetKeyMetrics(previousQuery, cancellationToken);
+ return Ok(new KeyMetrics(await current, previous));
+ }
+
+ [HttpGet("/api/_stats/periodic")]
+ public async Task PeriodicStats([FromQuery] QueryParams body, CancellationToken cancellationToken)
+ {
+ var query = body.Parse();
+ var rows = await _queryClient.NamedQueryAsync("key_metrics_periodic__v2", new {
+ date_from = query.DateFrom?.ToString("yyyy-MM-dd HH:mm:ss"),
+ date_to = query.DateTo?.ToString("yyyy-MM-dd HH:mm:ss"),
+ granularity = (query.Granularity ?? GranularityEnum.Hour).ToString(),
+ app_id = query.AppId,
+ event_name = query.EventName,
+ os_name = query.OsName,
+ app_version = query.AppVersion,
+ country_code = query.CountryCode,
+ device_model = query.DeviceModel,
+ }, cancellationToken);
+
+ return Ok(rows);
+ }
+
+ [HttpGet("/api/_stats/top-props")]
+ public async Task EventProps([FromQuery] QueryParams body, CancellationToken cancellationToken)
+ {
+ var query = body.Parse();
+ var rows = await _queryClient.NamedQueryAsync("top_props__v2", new {
+ date_from = query.DateFrom?.ToString("yyyy-MM-dd HH:mm:ss"),
+ date_to = query.DateTo?.ToString("yyyy-MM-dd HH:mm:ss"),
+ granularity = (body.Granularity ?? GranularityEnum.Hour).ToString(),
+ app_id = query.AppId,
+ event_name = query.EventName,
+ os_name = query.OsName,
+ app_version = query.AppVersion,
+ country_code = query.CountryCode,
+ device_model = query.DeviceModel,
+ }, cancellationToken);
+
+ return Ok(rows);
+ }
+
+ [HttpGet("/api/_stats/live-geo")]
+ public async Task LiveGeo([FromQuery] QueryParams body, CancellationToken cancellationToken)
+ {
+ var query = body.Parse();
+
+ var rows = await _queryClient.NamedQueryAsync("live_geo__v1", new {
+ app_id = query.AppId,
+ }, cancellationToken);
+
+ foreach (var row in rows)
+ {
+ var (lat, lng) = _geodb.GetLatLng(row.CountryCode, row.RegionName);
+ row.Latitude = lat;
+ row.Longitude = lng;
+ }
+
+ return Ok(rows);
+ }
+
+ [HttpGet("/api/_stats/live-sessions")]
+ public async Task LiveSessions([FromQuery] QueryParams body, CancellationToken cancellationToken)
+ {
+ var query = body.Parse();
+
+ var rows = await _queryClient.NamedQueryAsync("live_sessions__v1", new {
+ app_id = query.AppId,
+ }, cancellationToken);
+
+ return Ok(rows);
+ }
+
+ [HttpGet("/api/_stats/live-session-details")]
+ public async Task LiveSessionDetails([FromQuery] QueryParams body, CancellationToken cancellationToken)
+ {
+ var query = body.Parse();
+
+ var row = await _queryClient.NamedQuerySingleAsync("live_session_details__v1", new {
+ app_id = query.AppId,
+ session_id = query.SessionId,
+ }, cancellationToken);
+
+ return Ok(row);
+ }
+
+ [HttpGet("/api/_stats/historical-sessions")]
+ public async Task HistoricalSessions([FromQuery] QueryParams body, CancellationToken cancellationToken)
+ {
+ var query = body.Parse();
+
+ var row = await _queryClient.NamedQueryAsync("historical_sessions__v1", new
+ {
+ app_id = query.AppId,
+ session_id = query.SessionId,
+ date_from = query.DateFrom?.ToString("yyyy-MM-dd HH:mm:ss"),
+ date_to = query.DateTo?.ToString("yyyy-MM-dd HH:mm:ss"),
+ event_name = query.EventName,
+ os_name = query.OsName,
+ app_version = query.AppVersion,
+ country_code = query.CountryCode
+ }, cancellationToken);
+
+ return Ok(row);
+ }
+
+ private async Task GetKeyMetrics(QueryArgs args, CancellationToken cancellationToken)
+ {
+ return await _queryClient.NamedQuerySingleAsync("key_metrics__v2", new {
+ date_from = args.DateFrom?.ToString("yyyy-MM-dd HH:mm:ss"),
+ date_to = args.DateTo?.ToString("yyyy-MM-dd HH:mm:ss"),
+ granularity = (args.Granularity ?? GranularityEnum.Hour).ToString(),
+ app_id = args.AppId,
+ event_name = args.EventName,
+ os_name = args.OsName,
+ app_version = args.AppVersion,
+ country_code = args.CountryCode,
+ device_model = args.DeviceModel,
+ }, cancellationToken);
+ }
+
+ private async Task TopN(string nameColumn, GranularityEnum granularity, TopNValue valueColumn, QueryParams body, CancellationToken cancellationToken)
+ {
+ var query = body.Parse();
+ var rows = await _queryClient.NamedQueryAsync