dericke: @sgillies I think this is finally ready for review. I changed the way the namespaces are used so that the NS for Google's extensions could be used—hope that's acceptable.
diff --git a/.gitignore b/.gitignore
index dbf3f82..bb11b69 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@
 dist
 __pycache__
 *.egg-info
+.vscode/
+.pytest_cache/
\ No newline at end of file
diff --git a/keytree/factory.py b/keytree/factory.py
index 86f9275..2ab28ec 100644
--- a/keytree/factory.py
+++ b/keytree/factory.py
@@ -2,40 +2,46 @@
 Factories for features and geometries
 """
 
+from typing import Union
+
 import keytree.compat
-from keytree.model import Geometry, Feature, GEOM_TYPES
+from keytree.model import GEOM_TYPES, NSMAP, Feature, Geometry
 
 
-def feature(element):
-    kmlns = element.tag.split("}")[0][1:]
+def feature(element, kmlns: Union[str, dict] = NSMAP) -> Feature:
     kid = element.attrib.get("id")
-    name = element.findtext("{%s}name" % kmlns)
-    snippet = element.findtext("{%s}Snippet" % kmlns)
-    description = element.findtext("{%s}description" % kmlns)
+    name = element.findtext("kml:name", namespaces=kmlns) or element.findtext(
+        "kml:Name", namespaces=kmlns
+    )
+    snippet = element.findtext("kml:Snippet", namespaces=kmlns)
+    description = element.findtext(
+        "kml:description", namespaces=kmlns
+    ) or element.findtext("kml:Description", namespaces=kmlns)
     for geom_type in GEOM_TYPES:
-        tag = "{%s}%s" % (kmlns, geom_type)
-        geom_element = element.find(tag)
+        geom_element = element.find(geom_type, namespaces=kmlns)
         if geom_element is not None:
-            g = geometry(geom_element)
+            g = geometry(geom_element, kmlns=kmlns)
             return Feature(kid, g, name=name, snippet=snippet, description=description)
     return Feature(kid, None, name=name, snippet=snippet, description=description)
 
 
-def geometry(element):
+def geometry(element, kmlns: dict = NSMAP) -> Geometry:
     tp = element.tag.split("}")
-    kmlns = tp[0][1:]
+    if kmlns is None:
+        kmlns = {"": tp[0][1:]}
     geom_type = tp[1]
+    # geom_type = element.tag
     return geometry_factory[geom_type](element, kmlns)
 
 
-def geometry_Point(element, kmlns):
-    t = element.findtext("{%s}coordinates" % kmlns)
+def geometry_Point(element, kmlns: dict = NSMAP):
+    t = element.findtext("kml:coordinates", namespaces=kmlns)
     tv = t.split(",")
     return Geometry("Point", tuple([float(v) for v in tv]))
 
 
-def geometry_LineString(element, kmlns):
-    text = element.findtext("{%s}coordinates" % kmlns)
+def geometry_LineString(element, kmlns: dict = NSMAP):
+    text = element.findtext("kml:coordinates", namespaces=kmlns)
     ts = text.split()
     coords = []
     for t in ts:
@@ -44,9 +50,18 @@ def geometry_LineString(element, kmlns):
     return Geometry("LineString", tuple(coords))
 
 
-def geometry_Polygon(element, kmlns):
-    shell = element.find("{%s}outerBoundaryIs" % kmlns)
-    text = shell.findtext("*/{%s}coordinates" % kmlns)
+def geometry_Track(element, kmlns: dict = NSMAP):
+    sourcecoords = element.findall("gx:coord", namespaces=kmlns)
+    coords = []
+    for coord in sourcecoords:
+        tv = coord.text.split()
+        coords.append(tuple([float(v) for v in tv]))
+    return Geometry("LineString", tuple(coords))
+
+
+def geometry_Polygon(element, kmlns: dict = NSMAP):
+    shell = element.find("kml:outerBoundaryIs", namespaces=kmlns)
+    text = shell.findtext("*/kml:coordinates", namespaces=kmlns)
     ts = text.split()
     shell_coords = []
     for t in ts:
@@ -55,9 +70,9 @@ def geometry_Polygon(element, kmlns):
     poly_coords = []
     poly_coords.append(tuple(shell_coords))
 
-    holes = element.findall("{%s}innerBoundaryIs" % kmlns)
+    holes = element.findall("kml:innerBoundaryIs", namespaces=kmlns)
     for hole in holes:
-        text = hole.findtext("*/{%s}coordinates" % kmlns)
+        text = hole.findtext("*/kml:coordinates", namespaces=kmlns)
         ts = text.split()
         hole_coords = []
         for t in ts:
@@ -71,5 +86,6 @@ def geometry_Polygon(element, kmlns):
 geometry_factory = {
     "Point": geometry_Point,
     "LineString": geometry_LineString,
+    "Track": geometry_Track,
     "Polygon": geometry_Polygon,
 }
diff --git a/keytree/kml.py b/keytree/kml.py
index 8cc279d..a4a75c2 100644
--- a/keytree/kml.py
+++ b/keytree/kml.py
@@ -3,14 +3,23 @@ Functions and factories for KML elements
 """
 
 import keytree.compat
+from keytree.model import NSMAP
 
 
-def kml_ns(element):
+def kml_ns(element) -> str:
     return element.tag.split("}")[0][1:]
 
 
-def findall_placemarks(element):
-    return element.findall(".//{%s}Placemark" % kml_ns(element))
+def kml_ns_map(element) -> dict:
+    """Returns an nsmap-style dict detected from the given element
+    """
+    return {"": element.tag.split("}")[0][1:]}
+
+
+def findall_placemarks(element, kml_ns: dict = NSMAP) -> list:
+    """Returns a list of Placemark elements that are children of the given element
+    """
+    return element.findall(".//kml:Placemark", namespaces=kml_ns)
 
 
 def element(context, ob, **kw):
diff --git a/keytree/model.py b/keytree/model.py
index c78e17b..103a55d 100644
--- a/keytree/model.py
+++ b/keytree/model.py
@@ -2,7 +2,15 @@
 Geometry classes
 """
 
-GEOM_TYPES = ["Point", "LineString", "LinearRing", "Polygon", "MultiGeometry"]
+GEOM_TYPES = [
+    "kml:Point",
+    "kml:LineString",
+    "kml:LinearRing",
+    "kml:Polygon",
+    "kml:MultiGeometry",
+    "gx:Track",
+    # "gx:MultiTrack",
+]
 
 PROPERTIES_CONTEXT = {
     "name": "http://developers.google.com/kml/documentation/kmlreference#name",
@@ -10,8 +18,15 @@ PROPERTIES_CONTEXT = {
     "description": "http://developers.google.com/kml/documentation/kmlreference#description",
 }
 
+NSMAP = {
+    "": "http://www.opengis.net/kml/2.2",
+    "kml": "http://www.opengis.net/kml/2.2",
+    "gx": "http://www.google.com/kml/ext/2.2",
+    "atom": "http://www.w3.org/2005/Atom",
+}
+
 
-class Dictish(object):
+class Dictish:
     # Provides dict-like access to geo_interface items.
     def __getitem__(self, key):
         return self.__geo_interface__[key]

Feature request: support Google extension feature types
Some pro-bono work I'm doing has me converting features like [`gx:Track`](https://developers.google.com/kml/documentation/kmlreference#gx:track) into LineStrings for geojson output. I'm already writing the code to do so and would be happy to make it into a PR for this project, but I wanted to check if the interest is there before I write and submit it.
diff --git a/keytree/tests/test_read.py b/keytree/tests/test_read.py
index 455e820..56a1c63 100644
--- a/keytree/tests/test_read.py
+++ b/keytree/tests/test_read.py
@@ -1,9 +1,9 @@
 import pytest
 
-from keytree import feature
+from keytree import feature, kml
 
 KML = """<?xml version="1.0" encoding="UTF-8"?>
-<kml xmlns="http://www.opengis.net/kml/2.2">
+<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2">
   <Document>
     <Placemark>
       <name>point</name>
@@ -25,6 +25,17 @@ KML = """<?xml version="1.0" encoding="UTF-8"?>
         </coordinates>
       </LineString>
     </Placemark>
+    <Placemark>
+      <name>track</name>
+      <Snippet>Track test</Snippet>
+      <description>Blah, blah, blah</description>
+      <gx:Track>
+        <when>2020-12-13T07:33:02.94Z</when>
+        <when>2020-12-13T07:33:04.04Z</when>
+        <gx:coord>-122.364383 37.824664 0</gx:coord>
+        <gx:coord>-122.364152 37.824322 0</gx:coord>
+      </gx:Track>
+    </Placemark>
     <Placemark>
       <name>polygon</name>
       <Snippet>Polygon test</Snippet>
@@ -59,11 +70,6 @@ KML = """<?xml version="1.0" encoding="UTF-8"?>
 """.encode()  # lxml doesn't like unencoded strings with an encoding declaration
 
 
-#         self.placemarks = self.doc.findall(
-#             "*/{http://www.opengis.net/kml/2.2}Placemark"
-#         )
-
-
 @pytest.fixture
 def doc(etree):
     return etree.fromstring(KML)
@@ -71,13 +77,7 @@ def doc(etree):
 
 @pytest.fixture
 def placemarks(doc):
-    return doc.findall(
-        ".//kml:Placemark",
-        namespaces={
-            "": "http://www.opengis.net/kml/2.2",
-            "kml": "http://www.opengis.net/kml/2.2",
-        },
-    )
+    return kml.findall_placemarks(doc)
 
 
 def test_properties_context(placemarks):
@@ -116,8 +116,22 @@ def test_linestring(placemarks):
     )
 
 
-def test_polygon(placemarks):
+def test_track(placemarks):
     f = feature(placemarks[2])
+    assert f.geometry.type == f["geometry"]["type"] == "LineString"
+    coords0 = f.geometry.coordinates[0]
+    assert coords0 == pytest.approx((-122.36438, 37.82466, 0.0), 5)
+    assert f.properties["name"] == f["properties"]["name"] == "track"
+    assert f.properties["snippet"] == f["properties"]["snippet"] == "Track test"
+    assert (
+        f.properties["description"]
+        == f["properties"]["description"]
+        == "Blah, blah, blah"
+    )
+
+
+def test_polygon(placemarks):
+    f = feature(placemarks[3])
     assert f.geometry.type == f["geometry"]["type"] == "Polygon"
     coords0 = f.geometry.coordinates[0][0]
     assert coords0 == pytest.approx((-122.366278, 37.81884, 30.0), 5)

pip install -e .
parse_log_pytest
pytest
pytest lxml
apt-get update apt-get install -y gcc
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///croot/attrs_1668696182826/work
certifi @ file:///croot/certifi_1671487769961/work/certifi
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1648562407465/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
-e git+https://github.com/Toblerity/keytree.git@4820183ea6f10c8332b960e99d88e12a6e69092d#egg=keytree
lxml==5.3.1
packaging @ file:///croot/packaging_1671697413597/work
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pytest==7.1.2
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
typing_extensions @ file:///croot/typing_extensions_1669924550328/work
zipp @ file:///croot/zipp_1672387121353/work

name: keytree
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib-metadata=4.11.3=py37h06a4308_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - typing_extensions=4.4.0=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - lxml==5.3.1
prefix: /opt/conda/envs/keytree

keytree/tests/test_read.py::test_track[lxml] keytree/tests/test_read.py::test_track[ElementTree]
keytree/tests/test_read.py::test_properties_context[lxml] keytree/tests/test_read.py::test_properties_context[ElementTree] keytree/tests/test_read.py::test_point[lxml] keytree/tests/test_read.py::test_point[ElementTree] keytree/tests/test_read.py::test_linestring[lxml] keytree/tests/test_read.py::test_linestring[ElementTree] keytree/tests/test_read.py::test_polygon[lxml] keytree/tests/test_read.py::test_polygon[ElementTree]
.gitignore keytree/factory.py keytree/model.py keytree/kml.py
.gitignore keytree/factory.py keytree/model.py keytree/kml.py