Repository: goodmami/wn (language: python)

Issue:
Model and build ILIs as distinct from lexicon
~Anticipating a future~ (the future is [now](https://github.com/globalwordnet/cili)) where we have a versioned release of CILI, I would like to build the ILI database from that release and not pieced together from individual wordnets. This is where the ILI definition data would come from, and when reading an LMF, the ILIs associated with synsets would only be checked for validation (e.g., that all declared ILIs exist in the ILI table, otherwise throw a warning and ignore them).

This would require the release data to be published in some way. See https://github.com/globalwordnet/ili/issues/4
Approximate word search
Users should be able to query entries with approximate word form matches. At some level this falls outside the scope of this project, such as spelling correction, but things like morphological normalization (see #8) have traditionally been included. However, language-specific normalization should not be "baked-in". Things that are more cross-lingual are case-folding and diacritic stripping, e.g.:

```pycon
>>> from unicodedata import normalize, combining
>>> def normalize_form(s: str) -> str:
...     return ''.join(c for c in normalize('NFKD', s.lower()) if not combining(c))
... 
>>> normalize_form('Jalapeno')
'jalapeno'
>>> normalize_form('jalapeño')
'jalapeno'
>>> normalize_form('ハラペーニョ')
'ハラヘーニョ'
>>> normalize_form('Wałęsa')  # not everything is normalized as we might expect
'wałesa'
```

These normalized forms would be mapped to the original word forms in a separate table. For case-folding, SQLite's `LIKE` operator only case-folds ASCII characters, so it isn't sufficient.

As for the table, it could be a permanent part of the schema, with one consistent normalization method applied for all word forms, or there could be temporary tables created and populated at a user's request, e.g., with a custom normalization method. The latter option does not need a breaking schema change, but it would take time to create the table for each session.

If it doesn't complicate the query too much, it would be best to put exact matches in the results before approximate matches. Or maybe it should just be possible to disable approximate matching during queries.
Use fixed rowids for sense/synset relation types
Switching the current frozenset implementation of the synset and sense relation inventories to an Enum may have a few benefits. One of which is to have persistent database rowids, which may be useful if new relations are added and the database needs to be updated.

```python
class SynsetRelation(Enum):
    AGENT = 1
    ALSO = 2
    ATTRIBUTE = 3
    ....
```

This would not be without problems, namely how to lookup an entry by the canonical (lowercase) name, hence the following (which needs to be a classmethod because the subscripting works on the class, not the object):

```python
class SynsetRelation(Enum):
    @classmethod
    def lookup(cls, key: str):
        return cls[key.upper()]
    ...
```

With this in place, usage would look like this:

```python
>>> from wn.constants import SynsetRelation
>>> SynsetRelation.ANTONYM  # get by enum name
<SynsetRelation.ANTONYM: 71>
>>> SynsetRelation.ANTONYM.name
'ANTONYM'
>>> SynsetRelation.ANTONYM.canonical_name
'antonym'
>>> SynsetRelation.ANTONYM.id
71
>>> SynsetRelation(71)  # get by numeric id
<SynsetRelation.ANTONYM: 71>
>>> SynsetRelation.lookup('antonym')  # get by canonical name
<SynsetRelation.ANTONYM: 71>
```

Another benefit is that this could be a place where we hook in [gwadoc](https://github.com/globalwordnet/gwadoc/) for, e.g., definitions of relations. For example, it might look like this:

```python
>>> SynsetRelation.ANTONYM.definition
'An opposite and inherently incompatible word'
```
Database schema updates
This issue is to catalog the various changes needing to be made to the database schema, as I would like to avoid changing it frequently (most changes require a rebuild of the database).

- [x] CILI indexing changes (#23)
- [ ] ~SenseKey mapping (#55)~ (handle this outside the DB)
- [x] Reduce/remove lookup tables for constants (#75)
- [x] WN-LMF 1.0 remaining features (#65)
- [x] WN-LMF 1.1 additions (#7)
- [x] Lexicon id in extendable tables (#91)
- [x] ~Remove `lexicalized` from synsets and senses tables (#96)~
- [x] Lexicon column indicating local modifications (see #17)
Lexicon extensions in the database
The LMF 1.1 changes in #7 will allow lexicon extensions, which are lexicons that build on top of others. This issue is related to the database changes required to support them.

* ~Base lexicons should not be required to be installed, so foreign keys to the base lexicon should be nullable.~
* As much as possible, we should try to reuse existing tables (e.g., for `<ExternalLexicalEntry>`, just add the new `<Sense>` or `<Form>` rows with the new lexicon specified, no need for anything special).
* Lexicons need to specify their extension somehow. For this, a separate table is best, because it can have 4 columns: extension lexicon rowid (foreign key), base lexicon id (text), base lexicon version (text), base lexicon rowid (nullable foreign key). A UNIQUE constraint could ensure an extension only has 1 base lexicon, and this requirement could be lifted later if multiple bases ever become a thing.


Discussion / hints:


I got something mostly working, but now I think the complexity will become a maintenance burden. Here's the gist of it:

```python
from typing import Type, TypeVar
from enum import Enum


# Relation Types

T = TypeVar('T', bound='WnEnum')


class WnEnum(Enum):

    def __init__(self, id, canonical):
        self._value_ = id
        self._canonical = canonical

    def __str__(self):
        return f'{self._canonical}'

    @classmethod
    def lookup(cls: Type[T], key: str) -> T:
        """Return the relation member by its canonical name."""
        return cls[key.upper()]

    @property
    def id(self) -> int:
        """Return the database row id for the relation."""
        return self.value


class SenseRelation(WnEnum):
    ANTONYM = (1, 'antonym')
    ALSO = (2, 'also')
    PARTICIPLE = (3, 'participle')
    ...
```

One remaining problem is that while it's easy to get a member normally (`PartOfSpeech.NOUN`) or by its ID (`PartOfSpeech(1)`), it's not easy to get by its canonical value (`n`). The `lookup()` method just uppercases the full member name, which works for relations but not parts of speech or adjpositions. Another good thing here is that `str(member)` returns the canonical value so I don't need to do things like `isinstance()` checks when going to the database.

As for the main topic of this issue, which is fixed rowids for relations, this could be implemented using a regular dict instead of a frozenset for `SYNSET_RELATIONS` and `SENSE_RELATIONS`.
I ran a benchmark comparing the speed of using lookup tables in the database versus an enum without any lookup tables as I was concerned about the time Python would take to lookup the enum structure, then it's numeric value:

**current**

```python
with sqlite3.connect(db) as conn:
    conn.execute('''
        SELECT ss.id FROM synsets AS ss
          JOIN parts_of_speech AS p ON p.rowid = ss.pos_rowid
         WHERE p.pos = ?
    ''', ('n',))
```

**without lookup table**

```python
class POS(enum.Enum):
    N = 3

with sqlite3.connect(db) as conn:
    conn.execute('''
        SELECT ss.id FROM synsets AS ss
         WHERE ss.pos_rowid = ?
    ''', (POS.N.value,))
```

The Enum version took ~~only 86% of the time of~~ about the same time as using the lookup table, on average, so there's no worry about it being a performance hit. The lookup tables also help enforce consistency in the database, but then again they complicate queries with all the extra lookups each time.

(*edit*: I had only executed the query without fetching the results. When the results are actually fetched, the difference goes away and they take roughly the same time.)



Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9cde825..6f46d1a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,116 @@
 ## [Unreleased]
 
 
+## [v0.6.0]
+
+**Release date: 2021-03-04**
+
+### Added
+
+* For WN-LMF 1.0 support ([#65])
+  - `wn.Sense.frames()`
+  - `wn.Sense.adjposition()`
+  - `wn.Tag`
+  - `wn.Form.tags()`
+  - `wn.Count`
+  - `wn.Sense.counts()`
+* For ILI modeling ([#23])
+  - `wn.ILI` class
+  - `wn.Wordnet.ili()`
+  - `wn.Wordnet.ilis()`
+  - `wn.ili()`
+  - `wn.ilis()`
+  - `wn.project.Package.type` property
+  - Index entries of different types; default is `'wordnet'`, `'ili'`
+    is also available
+  - Support for detecting and loading ILI tab-separated-value exports;
+    not directly accessible through the public API at this time
+  - Support for adding ILI resources to the database
+  - A CILI index entry ([#23])
+* `wn.lmf` WN-LMF 1.1 support ([#7])
+  - `<Requires>`
+  - `<LexiconExtension>`, `<Extends>`, `<ExternalSynset>`,
+    `<ExternalLexicalEntry>`, `<ExternalSense>`,
+    `<ExternalLemma>`, `<ExternalForm>`
+  - `subcat` on `<Sense>`
+  - `members` on `<Synset>`
+  - `lexfile` on `<Synset>`
+  - `<Pronunciation>`
+  - `id` on `<Form>`
+  - New relations
+* Other WN-LMF 1.1 support
+  - `wn.Lexicon.requires()`
+  - `wn.Lexicon.extends()` ([#99])
+  - `wn.Lexicon.extensions()` ([#99])
+  - `wn.Pronunciation` ([#7])
+  - `wn.Form.pronunciations()` ([#7])
+  - `wn.Form.id` ([#7])
+  - `wn.Synset.lexfile()`
+* `wn.constants.SENSE_SYNSET_RELATIONS`
+* `wn.WnWarning` (related to [#92])
+* `wn.Lexicon.modified()` ([#17])
+
+### Fixed
+
+* Adding a wordnet with sense relations with invalid target IDs now
+  raises an error instead of ignoring the relation.
+* Detect LMF-vs-CILI projects even when files are uncompressed ([#104])
+
+### Changed
+
+* WN-LMF 1.0 entities now modeled and exported to XML ([#65]):
+  - Syntactic behaviour ([#65])
+  - Adjpositions ([#65])
+  - Form tags
+  - Sense counts
+  - Definition source senses
+  - ILI definitions
+* WN-LMF 1.1 entities now modeled and exported to XML ([#89]):
+  - Lexicon requirements and extensions ([#99])
+  - Form pronunciations
+  - Lexicographer files via the `lexfile` attribute
+  - Form ids
+* `wn.Synset.ili` now returns an `ILI` object
+* `wn.remove()` now takes a `progess_handler` parameter
+* `wn.util.ProgressBar` uses a simpler formatting string with two new
+  computed variables
+* `wn.project.is_package_directory()` and
+  `wn.project.is_collection_directory()` now detect
+  packages/collection with ILI resource files ([#23])
+* `wn.project.iterpackages()` now includes ILI packages
+* `wn.Wordnet` now sets the default `expand` value to a lexicon's
+  dependencies if they are specified (related to [#92])
+
+### Schema
+
+* General changes:
+  - Parts of speech are stored as text
+  - Added indexes and `ON DELETE` actions to speed up `wn.remove()`
+  - All extendable tables are now linked to their lexicon ([#91])
+  - Added rowid to tables with metadata
+  - Preemptively added a `modified` column to `lexicons` table ([#17])
+  - Preemptively added a `normalized_form` column to `forms` ([#105])
+  - Relation type tables are combined for synsets and senses ([#75])
+* ILI-related changes ([#23]):
+  - ILIs now have an integer rowid and a status
+  - Proposed ILIs also have an integer rowid for metadata access
+  - Added a table for ILI statuses
+* WN-LMF 1.0 changes ([#65]):
+  - SyntacticBehaviour (previously unused) no longer requires an ID and
+    does not use it in the primary key
+  - Added table for adjposition values
+  - Added source-sense to definitions table
+* WN-LMF 1.1 changes ([#7], [#89]):
+  - Added a table for lexicon dependencies
+  - Added a table for lexicon extensions ([#99])
+  - Added `logo` column to `lexicons` table
+  - Added a `synset_rank` column to `senses` table
+  - Added a `pronunciations` table
+  - Added column for lexicographer files to the `synsets` table
+  - Added a table for lexicographer file names
+  - Added an `id` column to `forms` table
+
+
 ## [v0.5.1]
 
 **Release date: 2021-01-29**
@@ -182,6 +292,7 @@ the https://github.com/nltk/wordnet/ code which had been effectively
 abandoned, but this is an entirely new codebase.
 
 
+[v0.6.0]: ../../releases/tag/v0.6.0
 [v0.5.1]: ../../releases/tag/v0.5.1
 [v0.5.0]: ../../releases/tag/v0.5.0
 [v0.4.1]: ../../releases/tag/v0.4.1
@@ -192,7 +303,10 @@ abandoned, but this is an entirely new codebase.
 [v0.1.0]: ../../releases/tag/v0.1.0
 [unreleased]: ../../tree/main
 
+[#7]: https://github.com/goodmami/wn/issues/7
 [#15]: https://github.com/goodmami/wn/issues/15
+[#17]: https://github.com/goodmami/wn/issues/17
+[#23]: https://github.com/goodmami/wn/issues/23
 [#47]: https://github.com/goodmami/wn/issues/47
 [#58]: https://github.com/goodmami/wn/issues/58
 [#59]: https://github.com/goodmami/wn/issues/59
@@ -200,12 +314,14 @@ abandoned, but this is an entirely new codebase.
 [#61]: https://github.com/goodmami/wn/issues/61
 [#63]: https://github.com/goodmami/wn/issues/63
 [#64]: https://github.com/goodmami/wn/issues/64
+[#65]: https://github.com/goodmami/wn/issues/65
 [#66]: https://github.com/goodmami/wn/issues/66
 [#69]: https://github.com/goodmami/wn/issues/69
 [#70]: https://github.com/goodmami/wn/issues/70
 [#71]: https://github.com/goodmami/wn/issues/71
 [#73]: https://github.com/goodmami/wn/issues/73
 [#74]: https://github.com/goodmami/wn/issues/74
+[#75]: https://github.com/goodmami/wn/issues/75
 [#76]: https://github.com/goodmami/wn/issues/76
 [#77]: https://github.com/goodmami/wn/issues/77
 [#78]: https://github.com/goodmami/wn/issues/78
@@ -214,7 +330,12 @@ abandoned, but this is an entirely new codebase.
 [#83]: https://github.com/goodmami/wn/issues/83
 [#86]: https://github.com/goodmami/wn/issues/86
 [#87]: https://github.com/goodmami/wn/issues/87
+[#89]: https://github.com/goodmami/wn/issues/89
 [#90]: https://github.com/goodmami/wn/issues/90
+[#91]: https://github.com/goodmami/wn/issues/91
 [#92]: https://github.com/goodmami/wn/issues/92
 [#93]: https://github.com/goodmami/wn/issues/93
 [#95]: https://github.com/goodmami/wn/issues/95
+[#99]: https://github.com/goodmami/wn/issues/99
+[#104]: https://github.com/goodmami/wn/issues/104
+[#105]: https://github.com/goodmami/wn/issues/105
diff --git a/README.md b/README.md
index 5034b3c..59821cb 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,6 @@
   <br>
   <a href="https://pypi.org/project/wn/"><img src="https://img.shields.io/pypi/v/wn.svg?style=flat-square" alt="PyPI link"></a>
   <img src="https://img.shields.io/pypi/pyversions/wn.svg?style=flat-square" alt="Python Support">
-  <img src="https://img.shields.io/pypi/dm/wn?style=flat-square" alt="Downloads">
   <a href="https://github.com/goodmami/wn/actions?query=workflow%3A%22tests%22"><img src="https://github.com/goodmami/wn/workflows/tests/badge.svg" alt="tests"></a>
   <a href="https://wn.readthedocs.io/en/latest/?badge=latest"><img src="https://readthedocs.org/projects/wn/badge/?version=latest&style=flat-square" alt="Documentation Status"></a>
   <br>
diff --git a/docs/api/wn.constants.rst b/docs/api/wn.constants.rst
index 78cd525..2112965 100644
--- a/docs/api/wn.constants.rst
+++ b/docs/api/wn.constants.rst
@@ -79,6 +79,20 @@ Synset Relations
    - ``subevent``
    - ``is_subevent_of``
    - ``antonym``
+   - ``feminine``
+   - ``has_feminine``
+   - ``masculine``
+   - ``has_masculine``
+   - ``young``
+   - ``has_young``
+   - ``diminutive``
+   - ``has_diminutive``
+   - ``augmentative``
+   - ``has_augmentative``
+   - ``anto_gradable``
+   - ``anto_simple``
+   - ``anto_converse``
+   - ``ir_synonym``
 
 
 Sense Relations
@@ -99,6 +113,31 @@ Sense Relations
    - ``is_exemplified_by``
    - ``similar``
    - ``other``
+   - ``feminine``
+   - ``has_feminine``
+   - ``masculine``
+   - ``has_masculine``
+   - ``young``
+   - ``has_young``
+   - ``diminutive``
+   - ``has_diminutive``
+   - ``augmentative``
+   - ``has_augmentative``
+   - ``anto_gradable``
+   - ``anto_simple``
+   - ``anto_converse``
+   - ``simple_aspect_ip``
+   - ``secondary_aspect_ip``
+   - ``simple_aspect_pi``
+   - ``secondary_aspect_pi``
+
+
+.. data:: SENSE_SYNSET_RELATIONS
+
+   - ``domain_topic``
+   - ``domain_region``
+   - ``exemplifies``
+   - ``other``
 
 
 Parts of Speech
diff --git a/docs/api/wn.rst b/docs/api/wn.rst
index 68bcaee..866b284 100644
--- a/docs/api/wn.rst
+++ b/docs/api/wn.rst
@@ -22,6 +22,8 @@ Wordnet Query Functions
 .. autofunction:: senses
 .. autofunction:: synset
 .. autofunction:: synsets
+.. autofunction:: ili
+.. autofunction:: ilis
 .. autofunction:: lexicons
 
 The Wordnet Class
@@ -35,6 +37,8 @@ The Wordnet Class
    .. automethod:: senses
    .. automethod:: synset
    .. automethod:: synsets
+   .. automethod:: ili
+   .. automethod:: ilis
    .. automethod:: lexicons
    .. automethod:: expanded_lexicons
 
@@ -61,18 +65,20 @@ The Word Class
 
 
 The Form Class
---------------
+''''''''''''''
 
 .. class:: Form
 
    The return value of :meth:`Word.lemma` and the members of the list
    returned by :meth:`Word.forms` are :class:`Form` objects. These are
    a basic subclass of Python's :class:`str` class with an additional
-   attribute, :attr:`script`. Form objects without any specified
-   script behave exactly as a regular string (they are equal and hash
-   to the same value), but if they have different script values they
-   are unequal and hash differently, even if the string itself is
-   identical.
+   attribute, :attr:`script`, and methods :meth:`pronunciations` and
+   :meth:`tags`. Form objects without any specified script behave
+   exactly as a regular string (they are equal and hash to the same
+   value), but if two Form objects are compared and they have
+   different script values, then they are unequal and hash
+   differently, even if the string itself is identical. When comparing
+   a Form object to a regular string, the script value is ignored.
 
    >>> inu = wn.words('犬', lexicon='wnja')[0]
    >>> inu.forms()[3]
@@ -89,6 +95,56 @@ The Form Class
       The script of the word form. This should be an `ISO 15924
       <https://en.wikipedia.org/wiki/ISO_15924>`_ code, or ``None``.
 
+   .. method:: pronunciations
+
+      Return the list of :class:`Pronunciation` objects.
+
+   .. method:: tags
+
+      Return the list of :class:`Tag` objects.
+
+
+The Pronunciation Class
+'''''''''''''''''''''''
+
+.. autoclass:: Pronunciation
+
+   .. attribute:: value
+
+      The encoded pronunciation.
+
+   .. attribute:: variety
+
+      The language variety this pronunciation belongs to.
+
+   .. attribute:: notation
+
+      The notation used to encode the pronunciation. For example: the
+      International Phonetic Alphabet (IPA).
+
+   .. attribute:: phonemic
+
+      :python:`True` when the encoded pronunciation is a generalized
+      phonemic description, or :python:`False` for more precise
+      phonetic transcriptions.
+
+   .. attribute:: audio
+
+      A URI to an associated audio file.
+
+The Tag Class
+'''''''''''''
+
+.. autoclass:: Tag
+
+   .. attribute:: tag
+
+      The text value of the tag.
+
+   .. attribute:: category
+
+      The category, or kind, of the tag.
+
 
 The Sense Class
 ---------------
@@ -101,6 +157,10 @@ The Sense Class
 
    .. automethod:: word
    .. automethod:: synset
+   .. automethod:: examples
+   .. automethod:: lexicalized
+   .. automethod:: adjposition
+   .. automethod:: frames
    .. automethod:: metadata
    .. automethod:: get_related
    .. automethod:: get_related_synsets
@@ -108,6 +168,21 @@ The Sense Class
    .. automethod:: relation_paths
    .. automethod:: translate
 
+
+The Count Class
+'''''''''''''''
+
+.. autoclass:: Count
+
+   Some wordnets store computed counts of senses across some corpus or
+   corpora. This class models those counts. It is a subtype of
+   :class:`int` with one additional method, :meth:`metadata`, which
+   may be used to give information about the source of the count (if
+   provided by the wordnet).
+
+   .. automethod:: metadata
+
+
 The Synset Class
 ----------------
 
@@ -129,6 +204,7 @@ The Synset Class
    .. automethod:: examples
    .. automethod:: senses
    .. automethod:: lexicalized
+   .. automethod:: lexfile
    .. automethod:: metadata
    .. automethod:: words
    .. automethod:: lemmas
@@ -147,6 +223,40 @@ The Synset Class
    .. automethod:: relation_paths
    .. automethod:: translate
 
+The ILI Class
+-------------
+
+.. autoclass:: ILI
+
+   .. attribute:: id
+
+      The interlingual index identifier. Unlike ``id`` attributes for
+      :class:`Word`, :class:`Sense`, and :class:`Synset`, ILI
+      identifers may be ``None`` (see the *proposed* :attr:`status`).
+
+   .. attribute:: status
+
+      The known status of the interlingual index. Loading an
+      interlingual index into the database provides the following
+      explicit, authoritative status values:
+
+      - ``active`` -- the ILI is in use
+      - ``provisional`` -- the ILI is being staged for permanent
+        inclusion
+      - ``deprecated`` -- the ILI is, or should be, no longer in use
+
+      Without an interlingual index loaded, ILIs present in loaded
+      lexicons get an implicit, temporary status from the following:
+
+      - ``presupposed`` -- a synset uses the ILI, assuming it exists
+        in an ILI file
+      - ``proposed`` -- a synset introduces a concept not yet in an
+        ILI and is suggesting that one should be added for it in the
+        future
+
+   .. automethod:: definition
+   .. automethod:: metadata
+
 The Lexicon Class
 -----------------
 
@@ -154,6 +264,7 @@ The Lexicon Class
 
    .. automethod:: metadata
    .. automethod:: specifier
+   .. automethod:: modified
 
 The wn.config Object
 --------------------
@@ -197,3 +308,4 @@ Exceptions
 ----------
 
 .. autoexception:: Error
+.. autoexception:: WnWarning
diff --git a/docs/api/wn.util.rst b/docs/api/wn.util.rst
index afd19d5..7246103 100644
--- a/docs/api/wn.util.rst
+++ b/docs/api/wn.util.rst
@@ -18,3 +18,4 @@ wn.util
       - ``status`` (:class:`str`) -- the current status of the process
 
 .. autoclass:: ProgressBar
+   :members:
diff --git a/docs/guides/basic.rst b/docs/guides/basic.rst
index 198c914..2bb57b2 100644
--- a/docs/guides/basic.rst
+++ b/docs/guides/basic.rst
@@ -253,7 +253,7 @@ rules are used:
 
 - A full ``id:version`` string (e.g., ``ewn:2020``) selects a specific
   lexicon
-- Only the a lexicon ``id`` (e.g., ``ewn``) selects the most recently
+- Only a lexicon ``id`` (e.g., ``ewn``) selects the most recently
   added lexicon with that ID
 - A star ``*`` may be used to match any lexicon; a star may not
   include a version
diff --git a/docs/guides/lexicons.rst b/docs/guides/lexicons.rst
index 1cb9bff..5337b9b 100644
--- a/docs/guides/lexicons.rst
+++ b/docs/guides/lexicons.rst
@@ -46,6 +46,7 @@ version, delimited by ``:``. Here are the possible forms:
     id          -- the most recently added lexicon with the given id
     id:*        -- all lexicons with the given id
     id:version  -- the lexicon with the given id and version
+    *:version   -- all lexicons with the given version
 
 For example, if both the ``2020`` and ``2019`` versions of the `Open
 English Wordnet`_ were installed, in that
@@ -60,7 +61,10 @@ downloaded and the lexicon that is installed), but sometimes it is
 not. The 1.3 release of the `Open Multilingual Wordnet`_, for
 instance, has the project specifier ``omw:1.3`` but it installs a
 number of lexicons with their own lexicon specifiers
-(``zsmwn:1.3+omw``, ``cmnwn:1.3+omw``, etc.).
+(``zsmwn:1.3+omw``, ``cmnwn:1.3+omw``, etc.). When only an id is given
+(e.g., ``ewn``), a project specifier gets the *first* version listed
+in the index (in the default index, the first version is the latest
+release).
 
 Downloading Lexicons
 --------------------
diff --git a/wn/__init__.py b/wn/__init__.py
index 0e3001a..eec001b 100644
--- a/wn/__init__.py
+++ b/wn/__init__.py
@@ -17,17 +17,24 @@
     'words',
     'Word',
     'Form',
+    'Pronunciation',
+    'Tag',
     'sense',
     'senses',
     'Sense',
+    'Count',
     'synset',
     'synsets',
     'Synset',
+    'ili',
+    'ilis',
+    'ILI',
     'Error',
+    'WnWarning',
 )
 
 from wn._meta import __version__
-from wn._exceptions import Error
+from wn._exceptions import Error, WnWarning
 from wn._config import config  # noqa: F401
 from wn._add import add, remove
 from wn._export import export
@@ -35,8 +42,9 @@
 from wn._core import (
     projects,
     lexicons, Lexicon,
-    word, words, Word, Form,
-    sense, senses, Sense,
+    word, words, Word, Form, Pronunciation, Tag,
+    sense, senses, Sense, Count,
     synset, synsets, Synset,
+    ili, ilis, ILI,
     Wordnet
 )
diff --git a/wn/__main__.py b/wn/__main__.py
index d797802..42c2c6f 100644
--- a/wn/__main__.py
+++ b/wn/__main__.py
@@ -27,8 +27,8 @@ def _projects(args):
                 key,
                 info['id'],
                 info['version'],
-                f"[{info['language']}]",
-                info['label'],
+                f"[{info['language'] or '---'}]",
+                info['label'] or '---',
             ))
         )
 
diff --git a/wn/_add.py b/wn/_add.py
index 10883f9..921a4a3 100644
--- a/wn/_add.py
+++ b/wn/_add.py
@@ -2,34 +2,45 @@
 Adding and removing lexicons to/from the database.
 """
 
-from typing import Optional, Type
+from typing import Optional, Type, Tuple, List
+from itertools import islice
 import logging
 
 import wn
 from wn._types import AnyPath
+from wn.constants import _WORDNET, _ILI
 from wn._db import connect
-from wn._queries import find_lexicons
+from wn._queries import find_lexicons, get_lexicon_extensions, get_lexicon
+from wn._util import normalize_form
 from wn.util import ProgressHandler, ProgressBar
 from wn.project import iterpackages
 from wn import lmf
+from wn import _ili
 
 
 logger = logging.getLogger('wn')
 
 
 BATCH_SIZE = 1000
+DEFAULT_MEMBER_RANK = 127  # synset member rank when not specified by 'members'
 
-POS_QUERY = '''
-    SELECT p.rowid
-      FROM parts_of_speech AS p
-     WHERE p.pos = ?
-'''
 ENTRY_QUERY = '''
     SELECT e.rowid
       FROM entries AS e
      WHERE e.id = ?
        AND e.lexicon_rowid = ?
 '''
+# forms don't have reliable ids, so also consider rank; this depends
+# on each form having a unique rank, and this doesn't work for lexicon
+# extensions
+FORM_QUERY = '''
+    SELECT f.rowid
+      FROM forms AS f
+      JOIN entries AS e ON f.entry_rowid = e.rowid
+     WHERE e.id = ?
+       AND e.lexicon_rowid = ?
+       AND (f.id = ? OR f.rank = ?)
+'''
 SENSE_QUERY = '''
     SELECT s.rowid
       FROM senses AS s
@@ -42,6 +53,21 @@
      WHERE ss.id = ?
        AND ss.lexicon_rowid = ?
 '''
+RELTYPE_QUERY = '''
+    SELECT rt.rowid
+      FROM relation_types AS rt
+     WHERE rt.type = ?
+'''
+ILISTAT_QUERY = '''
+    SELECT ist.rowid
+      FROM ili_statuses AS ist
+     WHERE ist.status = ?
+'''
+LEXFILE_QUERY = '''
+    SELECT lf.rowid
+      FROM lexfiles AS lf
+     WHERE lf.name = ?
+'''
 
 
 def add(
@@ -62,7 +88,7 @@ def add(
     """
     if progress_handler is None:
         progress_handler = ProgressHandler
-    progress = progress_handler(message='Database', unit='\b')
+    progress = progress_handler(message='Database')
 
     logger.info('adding project to database')
     logger.info('  database: %s', wn.config.database_path)
@@ -70,7 +96,12 @@ def add(
 
     try:
         for package in iterpackages(source):
-            _add_lmf(package.resource_file(), progress)
+            if package.type == _WORDNET:
+                _add_lmf(package.resource_file(), progress)
+            elif package.type == _ILI:
+                _add_ili(package.resource_file(), progress)
+            else:
+                raise wn.Error(f'unknown package type: {package.type}')
     finally:
         progress.close()
 
@@ -94,130 +125,241 @@ def _add_lmf(
         if not all_infos:
             progress.flash(f'{source}: No lexicons found')
             return
-        elif all(info.get('skip', False) for info in all_infos):
-            progress.flash(f'{source}: Some or all lexicons already added')
+        for info in all_infos:
+            skip = info.get('skip', '')
+            if skip:
+                id, ver, lbl = info['id'], info['version'], info['label']
+                progress.flash(f'Skipping {id}:{ver} ({lbl}); {skip}\n')
+        if all('skip' in info for info in all_infos):
             return
 
         # all clear, try to add them
+        progress.flash('Updating lookup tables')
+        _update_lookup_tables(all_infos, cur)
+
         progress.flash(f'Reading {source!s}')
         for lexicon, info in zip(lmf.load(source), all_infos):
-
-            if info.get('skip', False):
-                progress.flash(
-                    f'Skipping {info["id"]:info["version"]} ({info["label"]})\n',
-                )
+            if 'skip' in info:
                 continue
 
-            sense_ids = lexicon.sense_ids()
-            synset_ids = lexicon.synset_ids()
-
-            cur.execute(
-                'INSERT INTO lexicons VALUES (null,?,?,?,?,?,?,?,?,?)',
-                (lexicon.id,
-                 lexicon.label,
-                 lexicon.language,
-                 lexicon.email,
-                 lexicon.license,
-                 lexicon.version,
-                 lexicon.url,
-                 lexicon.citation,
-                 lexicon.meta))
-            lexid = cur.lastrowid
-
-            counts = info['counts']
-            count = sum(counts.get(name, 0) for name in
-                        ('LexicalEntry', 'Lemma', 'Form',  # 'Tag',
-                         'Sense', 'SenseRelation', 'Example',  # 'Count',
-                         # 'SyntacticBehaviour',
-                         'Synset', 'Definition',  # 'ILIDefinition',
-                         'SynsetRelation'))
-            count += counts.get('Synset', 0)  # again for ILIs
-            progress.set(count=0, total=count)
-
+            progress.set(count=0, total=_sum_counts(info))
             synsets = lexicon.synsets
             entries = lexicon.lexical_entries
+            synbhrs = lexicon.syntactic_behaviours
 
-            _insert_ilis(synsets, cur, progress)
-            _insert_synsets(synsets, lexid, cur, progress)
-            _insert_entries(entries, lexid, cur, progress)
-            _insert_forms(entries, lexid, cur, progress)
-            _insert_senses(entries, lexid, cur, progress)
+            lexid, qryid = _insert_lexicon(lexicon, info, cur, progress)
 
-            _insert_synset_relations(synsets, lexid, cur, progress)
-            _insert_sense_relations(entries, lexid, 'sense_relations',
-                                    sense_ids, cur, progress)
-            _insert_sense_relations(entries, lexid, 'sense_synset_relations',
-                                    synset_ids, cur, progress)
+            lexidmap = _build_lexid_map(lexicon, lexid, qryid)
 
-            _insert_synset_definitions(synsets, lexid, cur, progress)
+            _insert_synsets(synsets, lexid, cur, progress)
+            _insert_entries(entries, lexid, cur, progress)
+            _insert_forms(entries, lexid, lexidmap, cur, progress)
+            _insert_pronunciations(entries, lexid, lexidmap, cur, progress)
+            _insert_tags(entries, lexid, lexidmap, cur, progress)
+            _insert_senses(entries, synsets, lexid, lexidmap, cur, progress)
+            _insert_adjpositions(entries, lexid, lexidmap, cur, progress)
+            _insert_counts(entries, lexid, lexidmap, cur, progress)
+            _insert_syntactic_behaviours(synbhrs, lexid, lexidmap, cur, progress)
+
+            _insert_synset_relations(synsets, lexid, lexidmap, cur, progress)
+            _insert_sense_relations(lexicon, lexid, lexidmap, cur, progress)
+
+            _insert_synset_definitions(synsets, lexid, lexidmap, cur, progress)
             _insert_examples([sense for entry in entries for sense in entry.senses],
-                             lexid, 'sense_examples', cur, progress)
-            _insert_examples(synsets, lexid, 'synset_examples', cur, progress)
+                             lexid, lexidmap, 'sense_examples', cur, progress)
+            _insert_examples(synsets, lexid, lexidmap, 'synset_examples', cur, progress)
+
             progress.set(status='')  # clear type string
-            progress.flash(f'Added {lexicon.id}:{lexicon.version} ({lexicon.label})')
+            progress.flash(f'Added {lexicon.id}:{lexicon.version} ({lexicon.label})\n')
 
 
 def _precheck(source, cur):
+    lexqry = 'SELECT * FROM lexicons WHERE id = ? AND version = ?'
     for info in lmf.scan_lexicons(source):
         id = info['id']
         version = info['version']
-        if cur.execute(
-            'SELECT * FROM lexicons WHERE id = ? AND version = ?',
-            (id, version)
-        ).fetchone():
-            info['skip'] = True
+        base = info.get('extends')
+        if cur.execute(lexqry, (id, version)).fetchone():
+            info['skip'] = 'already added'
+        if base and cur.execute(lexqry, base).fetchone() is None:
+            info['skip'] = f'base lexicon ({base[0]}:{base[1]}) not available'
         yield info
 
 
-def _split(sequence):
-    i = 0
-    for j in range(0, len(sequence), BATCH_SIZE):
-        yield sequence[i:j]
-        i = j
-    yield sequence[i:]
+def _sum_counts(info) -> int:
+    counts = info['counts']
+    return sum(counts.get(name, 0) for name in
+               ('LexicalEntry', 'ExternalLexicalEntry',
+                'Lemma', 'Form', 'Pronunciation', 'Tag',
+                'Sense', 'ExternalSense',
+                'SenseRelation', 'Example', 'Count',
+                'SyntacticBehaviour',
+                'Synset', 'ExternalSynset',
+                'Definition',  # 'ILIDefinition',
+                'SynsetRelation'))
+
+
+def _update_lookup_tables(all_infos, cur):
+    reltypes = set(rt for info in all_infos for rt in info['relations'])
+    cur.executemany('INSERT OR IGNORE INTO relation_types VALUES (null,?)',
+                    [(rt,) for rt in sorted(reltypes)])
+    lexfiles = set(lf for info in all_infos for lf in info['lexfiles'])
+    cur.executemany('INSERT OR IGNORE INTO lexfiles VALUES (null,?)',
+                    [(lf,) for lf in sorted(lexfiles)])
+
+
+def _insert_lexicon(lexicon, info, cur, progress) -> Tuple[int, int]:
+    progress.set(status='Lexicon Info')
+    cur.execute(
+        'INSERT INTO lexicons VALUES (null,?,?,?,?,?,?,?,?,?,?,?)',
+        (lexicon.id,
+         lexicon.label,
+         lexicon.language,
+         lexicon.email,
+         lexicon.license,
+         lexicon.version,
+         lexicon.url,
+         lexicon.citation,
+         lexicon.logo,
+         lexicon.meta,
+         False))
+    lexid = cur.lastrowid
+
+    query = '''
+        UPDATE lexicon_dependencies
+           SET provider_rowid = ?
+         WHERE provider_id = ? AND provider_version = ?
+    '''
+    cur.execute(query, (lexid, lexicon.id, lexicon.version))
 
+    query = '''
+        INSERT INTO {table}
+        VALUES (:lid,
+                :id,
+                :version,
+                :url,
+                (SELECT rowid FROM lexicons WHERE id=:id AND version=:version))
+    '''
+    params = []
+    for dep in lexicon.requires:
+        param_dict = dict(dep)
+        param_dict.setdefault('url', None)
+        param_dict['lid'] = lexid
+        params.append(param_dict)
+    if params:
+        cur.executemany(query.format(table='lexicon_dependencies'), params)
+
+    if lexicon.extends:
+        param_dict = dict(lexicon.extends)
+        param_dict.setdefault('url', None)
+        param_dict['lid'] = lexid
+        cur.execute(query.format(table='lexicon_extensions'), param_dict)
+        qryid = cur.execute(
+            'SELECT rowid FROM lexicons WHERE id=? AND version=?',
+            (param_dict['id'], param_dict['version'])
+        ).fetchone()[0]
+    else:
+        qryid = lexid
+
+    return lexid, qryid
+
+
+def _build_lexid_map(lexicon, lexid, qryid):
+    lexidmap = {}
+    if lexid != qryid:
+        lexidmap.update((e.id, qryid) for e in lexicon.lexical_entries if e.external)
+        lexidmap.update((s.id, qryid)
+                        for e in lexicon.lexical_entries
+                        for s in e.senses
+                        if s.external)
+        lexidmap.update((ss.id, qryid) for ss in lexicon.synsets if ss.external)
+    return lexidmap
 
-def _insert_ilis(synsets, cur, progress):
-    progress.set(status='ILI')
-    for batch in _split(synsets):
-        data = (
-            (synset.ili,
-             synset.ili_definition.text if synset.ili_definition else None,
-             synset.ili_definition.meta if synset.ili_definition else None)
-            for synset in batch if synset.ili and synset.ili != 'in'
-        )
-        cur.executemany('INSERT OR IGNORE INTO ilis VALUES (?,?,?)', data)
-        progress.update(len(batch))
 
+def _split(sequence):
+    it = iter(sequence)
+    batch = list(islice(it, 0, BATCH_SIZE))
+    while len(batch):
+        yield batch
+        batch = list(islice(it, 0, BATCH_SIZE))
 
-def _insert_synsets(synsets, lex_id, cur, progress):
+
+def _insert_synsets(synsets, lexid, cur, progress):
     progress.set(status='Synsets')
-    query = f'INSERT INTO synsets VALUES (null,?,?,?,({POS_QUERY}),?,?)'
+    # synsets
+    ss_query = f'''
+        INSERT INTO synsets
+        VALUES (null,?,?,(SELECT rowid FROM ilis WHERE id=?),?,?,({LEXFILE_QUERY}),?)
+    '''
+    # presupposed ILIs
+    pre_ili_query = f'''
+        INSERT OR IGNORE INTO ilis
+        VALUES (null,?,({ILISTAT_QUERY}),?,?)
+    '''
+    # proposed ILIs
+    pro_ili_query = '''
+        INSERT INTO proposed_ilis
+        VALUES (null,
+               (SELECT ss.rowid
+                  FROM synsets AS ss
+                 WHERE ss.id=? AND lexicon_rowid=?),
+               ?,
+               ?)
+    '''
+
     for batch in _split(synsets):
+
+        # first add presupposed ILIs
+        data = []
+        for ss in batch:
+            ili = ss.ili
+            if ili and ili != 'in':
+                defn = ss.ili_definition
+                text = defn.text if defn else None
+                meta = defn.meta if defn else None
+                data.append((ili, 'presupposed', text, meta))
+        cur.executemany(pre_ili_query, data)
+
+        # then add synsets
         data = (
-            (synset.id,
-             lex_id,
-             synset.ili if synset.ili and synset.ili != 'in' else None,
-             synset.pos,
-             # lexfile_map.get(synset.meta.subject) if synset.meta else None,
-             synset.lexicalized,
-             synset.meta)
-            for synset in batch
+            (ss.id,
+             lexid,
+             ss.ili if ss.ili and ss.ili != 'in' else None,
+             ss.pos,
+             ss.lexicalized,
+             ss.lexfile,
+             ss.meta)
+            for ss in batch if not ss.external
         )
-        cur.executemany(query, data)
+        cur.executemany(ss_query, data)
+
+        # finally add proposed ILIs
+        data = []
+        for ss in batch:
+            ili = ss.ili
+            if ili == 'in':
+                defn = ss.ili_definition
+                text = defn.text if defn else None
+                meta = defn.meta if defn else None
+                data.append((ss.id, lexid, text, meta))
+        cur.executemany(pro_ili_query, data)
+
         progress.update(len(batch))
 
 
-def _insert_synset_definitions(synsets, lexid, cur, progress):
+def _insert_synset_definitions(synsets, lexid, lexidmap, cur, progress):
     progress.set(status='Definitions')
-    query = f'INSERT INTO definitions VALUES (({SYNSET_QUERY}),?,?,?)'
+    query = f'''
+        INSERT INTO definitions
+        VALUES (null,?,({SYNSET_QUERY}),?,?,({SENSE_QUERY}),?)
+    '''
     for batch in _split(synsets):
         data = [
-            (synset.id, lexid,
+            (lexid,
+             synset.id, lexidmap.get(synset.id, lexid),
              definition.text,
              definition.language,
-             # definition.source_sense,
-             # lexid,
+             definition.source_sense, lexidmap.get(definition.source_sense, lexid),
              definition.meta)
             for synset in batch
             for definition in synset.definitions
@@ -226,20 +368,17 @@ def _insert_synset_definitions(synsets, lexid, cur, progress):
         progress.update(len(data))
 
 
-def _insert_synset_relations(synsets, lexid, cur, progress):
+def _insert_synset_relations(synsets, lexid, lexidmap, cur, progress):
     progress.set(status='Synset Relations')
-    type_query = 'SELECT r.rowid FROM synset_relation_types AS r WHERE r.type = ?'
     query = f'''
         INSERT INTO synset_relations
-        VALUES (({SYNSET_QUERY}),
-                ({SYNSET_QUERY}),
-                ({type_query}),
-                ?)
+        VALUES (null,?,({SYNSET_QUERY}),({SYNSET_QUERY}),({RELTYPE_QUERY}),?)
     '''
     for batch in _split(synsets):
         data = [
-            (synset.id, lexid,
-             relation.target, lexid,
+            (lexid,
+             synset.id, lexidmap.get(synset.id, lexid),
+             relation.target, lexidmap.get(relation.target, lexid),
              relation.type,
              relation.meta)
             for synset in batch
@@ -249,36 +388,99 @@ def _insert_synset_relations(synsets, lexid, cur, progress):
         progress.update(len(data))
 
 
-def _insert_entries(entries, lex_id, cur, progress):
+def _insert_entries(entries, lexid, cur, progress):
     progress.set(status='Words')
-    query = f'INSERT INTO entries VALUES (null,?,?,({POS_QUERY}),?)'
+    query = 'INSERT INTO entries VALUES (null,?,?,?,?)'
     for batch in _split(entries):
         data = (
             (entry.id,
-             lex_id,
+             lexid,
              entry.lemma.pos,
              entry.meta)
-            for entry in batch
+            for entry in batch if not entry.external
         )
         cur.executemany(query, data)
         progress.update(len(batch))
 
 
-def _insert_forms(entries, lexid, cur, progress):
+def _insert_forms(entries, lexid, lexidmap, cur, progress):
     progress.set(status='Word Forms')
-    query = f'INSERT INTO forms VALUES (null,({ENTRY_QUERY}),?,?,?)'
+    query = f'INSERT INTO forms VALUES (null,?,?,({ENTRY_QUERY}),?,?,?,?)'
     for batch in _split(entries):
         forms = []
         for entry in batch:
-            forms.append((entry.id, lexid, entry.lemma.form, entry.lemma.script, 0))
-            forms.extend((entry.id, lexid, form.form, form.script, i)
-                         for i, form in enumerate(entry.forms, 1))
+            eid = entry.id
+            lemma = entry.lemma
+            lid = lexidmap.get(eid, lexid)
+            if not entry.external:
+                form = lemma.form
+                norm = normalize_form(form)
+                forms.append(
+                    (None, lexid, eid, lid,
+                     form, norm if norm != form else None,
+                     lemma.script, 0)
+                )
+            for i, form in enumerate(entry.forms, 1):
+                _form = form.form
+                norm = normalize_form(_form)
+                forms.append(
+                    (form.id, lexid, eid, lid,
+                     form.form, norm if norm != form else None,
+                     form.script, i)
+                )
         cur.executemany(query, forms)
         progress.update(len(forms))
 
 
-def _insert_senses(entries, lexid, cur, progress):
+def _insert_pronunciations(entries, lexid, lexidmap, cur, progress):
+    progress.set(status='Pronunciations')
+    query = f'INSERT INTO pronunciations VALUES (({FORM_QUERY}),?,?,?,?,?)'
+    for batch in _split(entries):
+        prons = []
+        for entry in batch:
+            eid = entry.id
+            lid = lexidmap.get(eid, lexid)
+            if entry.lemma:
+                for p in entry.lemma.pronunciations:
+                    prons.append(
+                        (eid, lid, None, 0,
+                         p.value, p.variety, p.notation, p.phonemic, p.audio)
+                    )
+            for i, form in enumerate(entry.forms, 1):
+                # rank is not valid in FORM_QUERY for external forms
+                rank = -1 if form.external else i
+                for p in form.pronunciations:
+                    prons.append(
+                        (eid, lid, form.id, rank,
+                         p.text, p.variety, p.notation, p.phonemic, p.audio)
+                    )
+        cur.executemany(query, prons)
+        progress.update(len(prons))
+
+
+def _insert_tags(entries, lexid, lexidmap, cur, progress):
+    progress.set(status='Word Form Tags')
+    query = f'INSERT INTO tags VALUES (({FORM_QUERY}),?,?)'
+    for batch in _split(entries):
+        tags = []
+        for entry in batch:
+            eid = entry.id
+            lid = lexidmap.get(eid, lexid)
+            if entry.lemma:
+                for tag in entry.lemma.tags:
+                    tags.append((eid, lid, None, 0, tag.text, tag.category))
+            for i, form in enumerate(entry.forms, 1):
+                # rank is not valid in FORM_QUERY for external forms
+                rank = -1 if form.external else i
+                for tag in form.tags:
+                    tags.append((eid, lid, form.id, rank, tag.text, tag.category))
+        cur.executemany(query, tags)
+        progress.update(len(tags))
+
+
+def _insert_senses(entries, synsets, lexid, lexidmap, cur, progress):
     progress.set(status='Senses')
+    ssrank = {s: i for ss in synsets for i, s in enumerate(ss.members)}
     query = f'''
         INSERT INTO senses
         VALUES (null,
@@ -288,58 +490,129 @@ def _insert_senses(entries, lexid, cur, progress):
                 ?,
                 ({SYNSET_QUERY}),
                 ?,
+                ?,
                 ?)
     '''
     for batch in _split(entries):
         data = [
             (sense.id,
              lexid,
-             entry.id, lexid,
+             entry.id, lexidmap.get(entry.id, lexid),
              i,
-             sense.synset, lexid,
-             # sense.meta.identifier if sense.meta else None,
-             # adjmap.get(sense.adjposition),
+             sense.synset, lexidmap.get(sense.synset, lexid),
+             ssrank.get(sense.id, DEFAULT_MEMBER_RANK),
              sense.lexicalized,
              sense.meta)
             for entry in batch
             for i, sense in enumerate(entry.senses)
+            if not sense.external
         ]
         cur.executemany(query, data)
         progress.update(len(data))
 
 
-def _insert_sense_relations(entries, lexid, table, ids, cur, progress):
-    progress.set(status='Sense Relations')
-    target_query = SENSE_QUERY if table == 'sense_relations' else SYNSET_QUERY
-    type_query = 'SELECT r.rowid FROM sense_relation_types AS r WHERE r.type = ?'
+def _insert_adjpositions(entries, lexid, lexidmap, cur, progress):
+    progress.set(status='Sense Adjpositions')
+    data = [(s.id, lexidmap.get(s.id, lexid), s.adjposition)
+            for e in entries
+            for s in e.senses
+            if s.adjposition and not s.external]
+    query = f'INSERT INTO adjpositions VALUES (({SENSE_QUERY}),?)'
+    cur.executemany(query, data)
+
+
+def _insert_counts(entries, lexid, lexidmap, cur, progress):
+    progress.set(status='Counts')
+    data = [(lexid,
+             sense.id, lexidmap.get(sense.id, lexid),
+             count.value,
+             count.meta)
+            for entry in entries
+            for sense in entry.senses
+            for count in sense.counts]
+    query = f'INSERT INTO counts VALUES (null,?,({SENSE_QUERY}),?,?)'
+    cur.executemany(query, data)
+    progress.update(len(data))
+
+
+def _insert_syntactic_behaviours(synbhrs, lexid, lexidmap, cur, progress):
+    progress.set(status='Syntactic Behaviours')
+    # syntactic behaviours don't have a required ID; index on frame
+    framemap = {}
+    for sb in synbhrs:
+        framemap.setdefault(sb.frame, []).extend(sb.senses)
+
+    query = 'INSERT INTO syntactic_behaviours VALUES (null,?,?,?)'
+    cur.executemany(query, [(None, lexid, frame) for frame in framemap])
+
     query = f'''
-        INSERT INTO {table}
-        VALUES (({SENSE_QUERY}),
-                ({target_query}),
-                ({type_query}),
-                ?)
+        INSERT INTO syntactic_behaviour_senses
+        VALUES ((SELECT rowid
+                   FROM syntactic_behaviours
+                  WHERE lexicon_rowid=? AND frame=?),
+                ({SENSE_QUERY}))
     '''
-    for batch in _split(entries):
-        data = [
-            (sense.id, lexid,
-             relation.target, lexid,
-             relation.type,
-             relation.meta)
-            for entry in batch
-            for sense in entry.senses
-            for relation in sense.relations if relation.target in ids
-        ]
-        # be careful of SQL injection here
-        cur.executemany(query, data)
-        progress.update(len(data))
+    data = [(lexid, frame, sid, lexidmap.get(sid, lexid))
+            for frame in framemap
+            for sid in framemap[frame]]
+    cur.executemany(query, data)
+
+    progress.update(len(synbhrs))
 
 
-def _insert_examples(objs, lexid, table, cur, progress):
+def _insert_sense_relations(lexicon, lexid, lexidmap, cur, progress):
+    progress.set(status='Sense Relations')
+    # need to separate relations into those targeting senses vs synsets
+    synset_ids = {ss.id for ss in lexicon.synsets}
+    sense_ids = {s.id for e in lexicon.lexical_entries for s in e.senses}
+    s_s_rels = []
+    s_ss_rels = []
+    for entry in lexicon.lexical_entries:
+        for sense in entry.senses:
+            slid = lexidmap.get(sense.id, lexid)
+            for relation in sense.relations:
+                target_id = relation.target
+                tlid = lexidmap.get(target_id, lexid)
+                if target_id in sense_ids:
+                    s_s_rels.append((sense.id, slid, tlid, relation))
+                elif target_id in synset_ids:
+                    s_ss_rels.append((sense.id, slid, tlid, relation))
+                else:
+                    raise wn.Error(
+                        f'relation target is not a known sense or synset: {target_id}'
+                    )
+    hyperparams = [
+        ('sense_relations', SENSE_QUERY, s_s_rels),
+        ('sense_synset_relations', SYNSET_QUERY, s_ss_rels),
+    ]
+    for table, target_query, rels in hyperparams:
+        query = f'''
+            INSERT INTO {table}
+            VALUES (null,?,({SENSE_QUERY}),({target_query}),({RELTYPE_QUERY}),?)
+        '''
+        for batch in _split(rels):
+            data = [
+                (lexid,
+                 sense_id, slid,
+                 relation.target, tlid,
+                 relation.type,
+                 relation.meta)
+                for sense_id, slid, tlid, relation in batch
+            ]
+            cur.executemany(query, data)
+            progress.update(len(data))
+
+
+def _insert_examples(objs, lexid, lexidmap, table, cur, progress):
     progress.set(status='Examples')
-    query = f'INSERT INTO {table} VALUES (({SYNSET_QUERY}),?,?,?)'
+    if table == 'sense_examples':
+        query = f'INSERT INTO {table} VALUES (null,?,({SENSE_QUERY}),?,?,?)'
+    else:
+        query = f'INSERT INTO {table} VALUES (null,?,({SYNSET_QUERY}),?,?,?)'
     for batch in _split(objs):
         data = [
-            (obj.id, lexid,
+            (lexid,
+             obj.id, lexidmap.get(obj.id, lexid),
              example.text,
              example.language,
              example.meta)
@@ -351,7 +624,44 @@ def _insert_examples(objs, lexid, table, cur, progress):
         progress.update(len(data))
 
 
-def remove(lexicon: str) -> None:
+def _add_ili(
+    source,
+    progress: ProgressHandler,
+) -> None:
+    query = f'''
+        INSERT INTO ilis
+        VALUES (null,?,({ILISTAT_QUERY}),?,null)
+            ON CONFLICT(id) DO
+               UPDATE SET status_rowid=excluded.status_rowid,
+                          definition=excluded.definition
+    '''
+    with connect() as conn:
+        cur = conn.cursor()
+
+        progress.flash(f'Reading ILI file: {source!s}')
+        ili = list(_ili.load(source))
+
+        progress.flash('Updating ILI Status Names')
+        statuses = set(info.get('status', 'active') for info in ili)
+        cur.executemany('INSERT OR IGNORE INTO ili_statuses VALUES (null,?)',
+                        [(stat,) for stat in sorted(statuses)])
+
+        progress.set(count=0, total=len(ili), status='ILI')
+        for batch in _split(ili):
+            data = [
+                (info['ili'],
+                 info.get('status', 'active'),
+                 info.get('definition'))
+                for info in batch
+            ]
+            cur.executemany(query, data)
+            progress.update(len(data))
+
+
+def remove(
+    lexicon: str,
+    progress_handler: Optional[Type[ProgressHandler]] = ProgressBar
+) -> None:
     """Remove lexicon(s) from the database.
 
     The *lexicon* argument is a :ref:`lexicon specifier
@@ -359,13 +669,43 @@ def remove(lexicon: str) -> None:
     project, so the lexicons of projects containing multiple lexicons
     will need to be removed individually.
 
+    The *progress_handler* parameter takes a subclass of
+    :class:`wn.util.ProgressHandler`. An instance of the class will be
+    created, used, and closed by this function.
+
     >>> wn.remove('ewn:2019')
 
     """
-    with connect() as conn:
+    if progress_handler is None:
+        progress_handler = ProgressHandler
+    progress = progress_handler(message='Removing', unit='\be5 operations')
+
+    conn = connect()
+    conn.set_progress_handler(progress.update, 100000)
+    try:
         for rowid, id, _, _, _, _, version, *_ in find_lexicons(lexicon=lexicon):
-            conn.execute('DELETE FROM entries WHERE lexicon_rowid = ?', (rowid,))
-            conn.execute('DELETE FROM synsets WHERE lexicon_rowid = ?', (rowid,))
-            conn.execute('DELETE FROM syntactic_behaviours WHERE lexicon_rowid = ?',
-                         (rowid,))
-            conn.execute('DELETE FROM lexicons WHERE rowid = ?', (rowid,))
+            extensions = _find_all_extensions(rowid)
+
+            with conn:
+
+                for ext_id, ext_spec in reversed(extensions):
+                    progress.set(status=f'{ext_spec} (extension)')
+                    conn.execute('DELETE from lexicons WHERE rowid = ?', (ext_id,))
+                    progress.flash(f'Removed {ext_spec}\n')
+
+                spec = f'{id}:{version}'
+                extra = f' (and {len(extensions)} extension(s))' if extensions else ''
+                progress.set(status=f'{spec}', count=0)
+                conn.execute('DELETE from lexicons WHERE rowid = ?', (rowid,))
+                progress.flash(f'Removed {spec}{extra}')
+
+    finally:
+        progress.close()
+
+
+def _find_all_extensions(rowid: int) -> List[Tuple[int, str]]:
+    exts: List[Tuple[int, str]] = []
+    for ext_id in get_lexicon_extensions(rowid):
+        lexinfo = get_lexicon(ext_id)
+        exts.append((ext_id, f'{lexinfo[1]}:{lexinfo[6]}'))
+    return exts
diff --git a/wn/_config.py b/wn/_config.py
index ae3fcbe..78d33c8 100644
--- a/wn/_config.py
+++ b/wn/_config.py
@@ -10,6 +10,7 @@
 
 from wn import Error
 from wn._types import AnyPath
+from wn.constants import _WORDNET
 from wn._util import is_url, resources, short_hash
 
 # The directory where downloaded and added data will be stored.
@@ -58,11 +59,12 @@ def index(self) -> Dict[str, Dict]:
         return self._projects
 
     def add_project(
-            self,
-            id: str,
-            label: str,
-            language: str,
-            license: str = None,
+        self,
+        id: str,
+        type: str = _WORDNET,
+        label: str = None,
+        language: str = None,
+        license: str = None,
     ) -> None:
         """Add a new wordnet project to the index.
 
@@ -77,6 +79,7 @@ def add_project(
         if id in self._projects:
             raise ValueError(f'project already added: {id}')
         self._projects[id] = {
+            'type': type,
             'label': label,
             'language': language,
             'versions': {},
@@ -137,6 +140,7 @@ def get_project_info(self, arg: str) -> Dict:
         return dict(
             id=id,
             version=version,
+            type=project['type'],
             label=project['label'],
             language=project['language'],
             license=versions[version].get('license', project.get('license')),
@@ -180,8 +184,9 @@ def update(self, data: dict) -> None:
             else:
                 self.add_project(
                     id,
-                    project['label'],
-                    project['language'],
+                    type=project.get('type', _WORDNET),
+                    label=project.get('label'),
+                    language=project.get('language'),
                     license=project.get('license'),
                 )
             for version, info in project.get('versions', {}).items():
diff --git a/wn/_core.py b/wn/_core.py
index 96b99d6..534d84e 100644
--- a/wn/_core.py
+++ b/wn/_core.py
@@ -1,5 +1,6 @@
 
 from typing import TypeVar, Optional, List, Tuple, Dict, Set, Iterator
+import warnings
 
 import wn
 from wn._types import Metadata
@@ -7,10 +8,18 @@
 from wn._db import NON_ROWID
 from wn._queries import (
     find_lexicons,
+    find_ilis,
+    find_proposed_ilis,
     find_entries,
     find_senses,
     find_synsets,
     get_lexicon,
+    get_modified,
+    get_lexicon_dependencies,
+    get_lexicon_extension_bases,
+    get_lexicon_extensions,
+    get_form_pronunciations,
+    get_form_tags,
     get_entry_senses,
     get_sense_relations,
     get_sense_synset_relations,
@@ -19,8 +28,12 @@
     get_synsets_for_ilis,
     get_examples,
     get_definitions,
+    get_syntactic_behaviours,
     get_metadata,
     get_lexicalized,
+    get_adjposition,
+    get_sense_counts,
+    get_lexfile,
 )
 
 _FAKE_ROOT = '*ROOT*'
@@ -56,6 +69,35 @@ def __hash__(self):
         return hash((self._ENTITY_TYPE, self._id))
 
 
+class ILI(_DatabaseEntity):
+    """A class for interlingual indices."""
+    __slots__ = 'id', 'status', '_definition'
+    __module__ = 'wn'
+
+    def __init__(
+        self,
+        id: Optional[str],
+        status: str,
+        definition: str = None,
+        _id: int = NON_ROWID,
+    ):
+        super().__init__(_id=_id)
+        self.id = id
+        self.status = status
+        self._definition = definition
+
+    def __repr__(self) -> str:
+        return f'ILI({repr(self.id) if self.id else "*PROPOSED*"})'
+
+    def definition(self) -> Optional[str]:
+        return self._definition
+
+    def metadata(self) -> Metadata:
+        """Return the ILI's metadata."""
+        table = 'proposed_ilis' if self.status == 'proposed' else 'ilis'
+        return get_metadata(self._id, table)
+
+
 class Lexicon(_DatabaseEntity):
     """A class representing a wordnet lexicon.
 
@@ -68,24 +110,26 @@ class Lexicon(_DatabaseEntity):
         version: The version string of the resource.
         url: The project URL of the wordnet.
         citation: The canonical citation for the project.
+        logo: A URL or path to a project logo.
     """
     __slots__ = ('id', 'label', 'language', 'email', 'license',
-                 'version', 'url', 'citation')
+                 'version', 'url', 'citation', 'logo')
     __module__ = 'wn'
 
     _ENTITY_TYPE = 'lexicons'
 
     def __init__(
-            self,
-            id: str,
-            label: str,
-            language: str,
-            email: str,
-            license: str,
-            version: str,
-            url: str = None,
-            citation: str = None,
-            _id: int = NON_ROWID,
+        self,
+        id: str,
+        label: str,
+        language: str,
+        email: str,
+        license: str,
+        version: str,
+        url: str = None,
+        citation: str = None,
+        logo: str = None,
+        _id: int = NON_ROWID,
     ):
         super().__init__(_id=_id)
         self.id = id
@@ -96,6 +140,7 @@ def __init__(
         self.version = version
         self.url = url
         self.citation = citation
+        self.logo = logo
 
     def __repr__(self):
         id, ver, lg = self.id, self.version, self.language
@@ -109,6 +154,42 @@ def specifier(self) -> str:
         """Return the *id:version* lexicon specifier."""
         return f'{self.id}:{self.version}'
 
+    def modified(self) -> bool:
+        """Return True if the lexicon has local modifications."""
+        return get_modified(self._id)
+
+    def requires(self) -> Dict[str, Optional['Lexicon']]:
+        """Return the lexicon dependencies."""
+        return dict(
+            (f'{id}:{version}',
+             None if _id is None else _to_lexicon(get_lexicon(_id)))
+            for id, version, _, _id in get_lexicon_dependencies(self._id)
+        )
+
+    def extends(self) -> Optional['Lexicon']:
+        """Return the lexicon this lexicon extends, if any.
+
+        If this lexicon is not an extension, return None.
+        """
+        bases = get_lexicon_extension_bases(self._id, depth=1)
+        if bases:
+            return _to_lexicon(get_lexicon(bases[0]))
+        return None
+
+    def extensions(self, depth: int = 1) -> List['Lexicon']:
+        """Return the list of lexicons extending this one.
+
+        By default, only direct extensions are included. This is
+        controlled by the *depth* parameter, which if you view
+        extensions as children in a tree where the current lexicon is
+        the root, *depth=1* are the immediate extensions. Increasing
+        this number gets extensions of extensions, or setting it to a
+        negative number gets all "descendant" extensions.
+
+        """
+        return [_to_lexicon(get_lexicon(rowid))
+                for rowid in get_lexicon_extensions(self._id, depth=depth)]
+
 
 class _LexiconElement(_DatabaseEntity):
     __slots__ = '_lexid', '_wordnet'
@@ -121,29 +202,85 @@ def __init__(
     ):
         super().__init__(_id=_id)
         self._lexid = _lexid  # Database-internal lexicon id
-        self._wordnet = _wordnet
+        if _wordnet is None:
+            _wordnet = Wordnet()
+        self._wordnet: 'Wordnet' = _wordnet
 
     def lexicon(self):
         return _to_lexicon(get_lexicon(self._lexid))
 
+    def _get_lexicon_ids(self) -> Tuple[int, ...]:
+        if self._wordnet._default_mode:
+            return tuple(
+                {self._lexid}
+                | set(get_lexicon_extension_bases(self._lexid))
+                | set(get_lexicon_extensions(self._lexid))
+            )
+        else:
+            return self._wordnet._lexicon_ids
+
+
+class Pronunciation:
+    """A class for word form pronunciations."""
+
+    __slots__ = 'value', 'variety', 'notation', 'phonemic', 'audio'
+
+    def __init__(
+        self,
+        value: str,
+        variety: str = None,
+        notation: str = None,
+        phonemic: bool = True,
+        audio: str = None,
+    ):
+        self.value = value
+        self.variety = variety
+        self.notation = notation
+        self.phonemic = phonemic
+        self.audio = audio
+
+
+class Tag:
+    """A general-purpose tag class for word forms."""
+    __slots__ = 'tag', 'category',
+    __module__ = 'wn'
+
+    def __init__(self, tag: str, category: str):
+        self.tag = tag
+        self.category = category
+
+    def __eq__(self, other):
+        if not isinstance(other, Tag):
+            return NotImplemented
+        return self.tag == other.tag and self.category == other.category
+
 
 class Form(str):
     """A word-form string with additional attributes."""
-    __slots__ = '_id', 'script',
+    __slots__ = '_id', 'id', 'script',
     __module__ = 'wn'
 
     _id: int
+    id: Optional[str]
     script: Optional[str]
 
-    def __new__(cls, form: str, script: str = None, _id: int = NON_ROWID):
+    def __new__(
+        cls,
+        form: str,
+        id: str = None,
+        script: str = None,
+        _id: int = NON_ROWID
+    ):
         obj = str.__new__(cls, form)  # type: ignore
+        obj.id = id
         obj.script = script
         obj._id = _id
         return obj
 
     def __eq__(self, other):
-        return (str.__eq__(self, other)
-                and self.script == getattr(other, 'script', None))
+        if isinstance(other, Form) and self.script != other.script:
+            return False
+        return str.__eq__(self, other)
 
     def __hash__(self):
         script = self.script
@@ -151,6 +288,12 @@ def __hash__(self):
             return str.__hash__(self)
         return hash((str(self), self.script))
 
+    def pronunciations(self) -> List[Pronunciation]:
+        return [Pronunciation(*data) for data in get_form_pronunciations(self._id)]
+
+    def tags(self) -> List[Tag]:
+        return [Tag(tag, category) for tag, category in get_form_tags(self._id)]
+
 
 class Word(_LexiconElement):
     """A class for words (also called lexical entries) in a wordnet."""
@@ -163,7 +306,7 @@ def __init__(
             self,
             id: str,
             pos: str,
-            forms: List[Tuple[str, Optional[str], int]],
+            forms: List[Tuple[str, Optional[str], Optional[str], int]],
             _lexid: int = NON_ROWID,
             _id: int = NON_ROWID,
             _wordnet: 'Wordnet' = None
@@ -207,7 +350,8 @@ def senses(self) -> List['Sense']:
             [Sense('ewn-zygoma-n-05292350-01')]
 
         """
-        iterable = get_entry_senses(self._id)
+        lexids = self._get_lexicon_ids()
+        iterable = get_entry_senses(self._id, lexids)
         return [Sense(*sense_data, self._wordnet) for sense_data in iterable]
 
     def metadata(self) -> Metadata:
@@ -318,7 +462,7 @@ def relation_paths(self: T, *args: str, end: T = None) -> Iterator[List[T]]:
 
 class Synset(_Relatable):
     """Class for modeling wordnet synsets."""
-    __slots__ = 'pos', 'ili'
+    __slots__ = 'pos', '_ili'
     __module__ = 'wn'
 
     _ENTITY_TYPE = 'synsets'
@@ -334,7 +478,7 @@ def __init__(
     ):
         super().__init__(id=id, _lexid=_lexid, _id=_id, _wordnet=_wordnet)
         self.pos = pos
-        self.ili = ili
+        self._ili = ili
 
     @classmethod
     def empty(
@@ -346,10 +490,20 @@ def empty(
     ):
         return cls(id, pos='', ili=ili, _lexid=_lexid, _wordnet=_wordnet)
 
+    @property
+    def ili(self):
+        if self._ili:
+            row = next(find_ilis(id=self._ili), None)
+        else:
+            row = next(find_proposed_ilis(synset_id=self._id), None)
+        if row is not None:
+            return ILI(*row)
+        return None
+
     def __hash__(self):
         # include ili and lexid in the hash so inferred synsets don't
         # hash the same
-        return hash((self._ENTITY_TYPE, self.ili, self._lexid, self._id))
+        return hash((self._ENTITY_TYPE, self._ili, self._lexid, self._id))
 
     def __repr__(self) -> str:
         return f'Synset({self.id!r})'
@@ -363,8 +517,9 @@ def definition(self) -> Optional[str]:
             'a wheel that has wooden spokes and a metal rim'
 
         """
+        lexids = self._get_lexicon_ids()
         return next(
-            (text for text, _, _ in get_definitions(self._id)),
+            (text for text, _, _, _ in get_definitions(self._id, lexids)),
             None
         )
 
@@ -377,7 +532,8 @@ def examples(self) -> List[str]:
             ['"orbital revolution"', '"orbital velocity"']
 
         """
-        exs = get_examples(self._id, 'synsets')
+        lexids = self._get_lexicon_ids()
+        exs = get_examples(self._id, 'synsets', lexids)
         return [ex for ex, _, _ in exs]
 
     def senses(self) -> List['Sense']:
@@ -389,12 +545,18 @@ def senses(self) -> List['Sense']:
             [Sense('ewn-umbrella-n-04514450-01')]
 
         """
-        iterable = get_synset_members(self._id)
+        lexids = self._get_lexicon_ids()
+        iterable = get_synset_members(self._id, lexids)
         return [Sense(*sense_data, self._wordnet) for sense_data in iterable]
 
     def lexicalized(self) -> bool:
+        """Return True if the synset is lexicalized."""
         return get_lexicalized(self._id, 'synsets')
 
+    def lexfile(self) -> Optional[str]:
+        """Return the lexicographer file name for this synset, if any."""
+        return get_lexfile(self._id)
+
     def metadata(self) -> Metadata:
         """Return the synset's metadata."""
         return get_metadata(self._id, 'synsets')
@@ -423,24 +585,25 @@ def lemmas(self) -> List[Form]:
 
     def get_related(self, *args: str) -> List['Synset']:
         targets: List['Synset'] = []
+
+        lexids = self._get_lexicon_ids()
+
         # first get relations from the current lexicon(s)
         if self._id != NON_ROWID:
+            relations = get_synset_relations({self._id}, args, lexids)
             targets.extend(Synset(*row[2:], self._wordnet)
-                           for row in get_synset_relations({self._id}, args))
+                           for row in relations
+                           if row[5] in lexids)
 
         # then attempt to expand via ILI
-        if self.ili is not None and self._wordnet and self._wordnet._expanded_ids:
-            lexids: Tuple[int, ...]
-            if self._wordnet._default_mode:
-                lexids = (self._lexid,)
-            else:
-                lexids = self._wordnet._lexicon_ids
+        if self._ili is not None and self._wordnet and self._wordnet._expanded_ids:
             expids = self._wordnet._expanded_ids
 
             # get expanded relation
-            expss = find_synsets(ili=self.ili, lexicon_rowids=expids)
+            expss = find_synsets(ili=self._ili, lexicon_rowids=expids)
             rowids = {rowid for _, _, _, _, rowid in expss} - {self._id, NON_ROWID}
-            ilis = {row[4] for row in get_synset_relations(rowids, args)} - {None}
+            relations = get_synset_relations(rowids, args, expids)
+            ilis = {row[4] for row in relations} - {None}
 
             # map back to target lexicons
             seen = {ss._id for ss in targets}
@@ -449,7 +612,7 @@ def get_related(self, *args: str) -> List['Synset']:
                     targets.append(Synset(*row, self._wordnet))
 
             # add empty synsets for ILIs without a target in lexids
-            for ili in (ilis - {tgt.ili for tgt in targets}):
+            for ili in (ilis - {tgt._ili for tgt in targets}):
                 targets.append(
                     Synset.empty(
                         id=_INFERRED_SYNSET,
@@ -702,12 +865,28 @@ def translate(self, lexicon: str = None, *, lang: str = None) -> List['Synset']:
 
         """
 
-        ili = self.ili
+        ili = self._ili
         if not ili:
             return []
         return synsets(ili=ili, lang=lang, lexicon=lexicon)
 
 
+class Count(int):
+    """A count of sense occurrences in some corpus."""
+    __module__ = 'wn'
+
+    _id: int
+
+    def __new__(cls, value, _id: int = NON_ROWID):
+        obj = int.__new__(cls, value)  # type: ignore
+        obj._id = _id
+        return obj
+
+    def metadata(self) -> Metadata:
+        """Return the count's metadata."""
+        return get_metadata(self._id, 'counts')
+
+
 class Sense(_Relatable):
     """Class for modeling wordnet senses."""
     __slots__ = '_entry_id', '_synset_id'
@@ -755,12 +934,38 @@ def synset(self) -> Synset:
 
     def examples(self) -> List[str]:
         """Return the list of examples for the sense."""
-        exs = get_examples(self._id, 'senses')
+        lexids = self._get_lexicon_ids()
+        exs = get_examples(self._id, 'senses', lexids)
         return [ex for ex, _, _ in exs]
 
     def lexicalized(self) -> bool:
+        """Return True if the sense is lexicalized."""
         return get_lexicalized(self._id, 'senses')
 
+    def adjposition(self) -> Optional[str]:
+        """Return the adjective position of the sense.
+
+        Values include :python:`"a"` (attributive), :python:`"p"`
+        (predicative), and :python:`"ip"` (immediate
+        postnominal). Note that this is only relevant for adjectival
+        senses. Senses for other parts of speech, or for adjectives
+        that are not annotated with this feature, will return
+        ``None``.
+
+        """
+        return get_adjposition(self._id)
+
+    def frames(self) -> List[str]:
+        """Return the list of subcategorization frames for the sense."""
+        lexids = self._get_lexicon_ids()
+        return get_syntactic_behaviours(self._id, lexids)
+
+    def counts(self) -> List[Count]:
+        """Return the corpus counts stored for this sense."""
+        lexids = self._get_lexicon_ids()
+        return [Count(value, _id=_id)
+                for value, _id in get_sense_counts(self._id, lexids)]
+
     def metadata(self) -> Metadata:
         """Return the sense's metadata."""
         return get_metadata(self._id, 'senses')
@@ -782,15 +987,19 @@ def get_related(self, *args: str) -> List['Sense']:
             incoherent
 
         """
-        iterable = get_sense_relations(self._id, args)
+        lexids = self._get_lexicon_ids()
+        iterable = get_sense_relations(self._id, args, lexids)
         return [Sense(sid, eid, ssid, lexid, rowid, self._wordnet)
-                for _, _, sid, eid, ssid, lexid, rowid in iterable]
+                for _, _, sid, eid, ssid, lexid, rowid in iterable
+                if lexids is None or lexid in lexids]
 
     def get_related_synsets(self, *args: str) -> List[Synset]:
         """Return a list of related synsets."""
-        iterable = get_sense_synset_relations(self._id, args)
+        lexids = self._get_lexicon_ids()
+        iterable = get_sense_synset_relations(self._id, args, lexids)
         return [Synset(ssid, pos, ili, lexid, rowid, self._wordnet)
-                for _, _, ssid, pos, ili, lexid, rowid in iterable]
+                for _, _, ssid, pos, ili, lexid, rowid in iterable
+                if lexids is None or lexid in lexids]
 
     def translate(self, lexicon: str = None, *, lang: str = None) -> List['Sense']:
         """Return a list of translated senses.
@@ -851,7 +1060,23 @@ def __init__(self, lexicon: str = None, *, lang: str = None, expand: str = None)
         if expand is None:
             if self._default_mode:
                 expand = '*'
-            # TODO: use project-specific settings
+            else:
+                deps = [(id, ver, _id)
+                        for lex in self._lexicons
+                        for id, ver, _, _id in get_lexicon_dependencies(lex._id)]
+                # warn only if a dep is missing and a lexicon was specified
+                if not self._default_mode:
+                    missing = ' '.join(
+                        f'{id}:{ver}' for id, ver, _id in deps if _id is None
+                    )
+                    if missing:
+                        warnings.warn(
+                            f'lexicon dependencies not available: {missing}',
+                            wn.WnWarning
+                        )
+                expand = ' '.join(
+                    f'{id}:{ver}' for id, ver, _id in deps if _id is not None
+                )
         if expand:
             self._expanded = tuple(map(_to_lexicon, find_lexicons(lexicon=expand)))
         self._expanded_ids: Tuple[int, ...] = tuple(lx._id for lx in self._expanded)
@@ -931,9 +1156,21 @@ def senses(self, form: str = None, pos: str = None) -> List[Sense]:
         iterable = find_senses(form=form, pos=pos, lexicon_rowids=self._lexicon_ids)
         return [Sense(*sense_data, self) for sense_data in iterable]
 
+    def ili(self, id: str) -> ILI:
+        """Return the first ILI in this wordnet with identifer *id*."""
+        iterable = find_ilis(id=id, lexicon_rowids=self._lexicon_ids)
+        try:
+            return ILI(*next(iterable))
+        except StopIteration:
+            raise wn.Error(f'no such ILI: {id}')
+
+    def ilis(self, status: str = None) -> List[ILI]:
+        iterable = find_ilis(status=status, lexicon_rowids=self._lexicon_ids)
+        return [ILI(*ili_data) for ili_data in iterable]
+
 
 def _to_lexicon(data) -> Lexicon:
-    rowid, id, label, language, email, license, version, url, citation = data
+    rowid, id, label, language, email, license, version, url, citation, logo = data
     return Lexicon(
         id,
         label,
@@ -943,6 +1180,7 @@ def _to_lexicon(data) -> Lexicon:
         version,
         url=url,
         citation=citation,
+        logo=logo,
         _id=rowid
     )
 
@@ -981,8 +1219,6 @@ def lexicons(*, lexicon: str = None, lang: str = None) -> List[Lexicon]:
         [<Lexicon ewn:2020 [en]>, <Lexicon pwn:3.0 [en]>]
 
     """
-    if lexicon is None:
-        lexicon = '*'
     try:
         w = Wordnet(lang=lang, lexicon=lexicon)
     except wn.Error:
@@ -1100,3 +1336,44 @@ def sense(id: str, *, lexicon: str = None, lang: str = None) -> Sense:
 
     """
     return Wordnet(lang=lang, lexicon=lexicon).sense(id=id)
+
+
+def ili(id: str, *, lexicon: str = None, lang: str = None) -> ILI:
+    """Return the interlingual index with *id*.
+
+    This will create a :class:`Wordnet` object using the *lang* and
+    *lexicon* arguments. The *id* argument is then passed to the
+    :meth:`Wordnet.ili` method.
+
+    >>> wn.ili(id='i1234')
+    ILI('i1234')
+    >>> wn.ili(id='i1234').status
+    'presupposed'
+
+    """
+    return Wordnet(lang=lang, lexicon=lexicon).ili(id=id)
+
+
+def ilis(
+    status: str = None,
+    *,
+    lexicon: str = None,
+    lang: str = None,
+) -> List[ILI]:
+    """Return the list of matching interlingual indices.
+
+    This will create a :class:`Wordnet` object using the *lang* and
+    *lexicon* arguments. The remaining arguments are passed to the
+    :meth:`Wordnet.ilis` method.
+
+    >>> len(wn.ilis())
+    120071
+    >>> len(wn.ilis(status='proposed'))
+    2573
+    >>> wn.ilis(status='proposed')[-1].definition()
+    'the neutrino associated with the tau lepton.'
+    >>> len(wn.ilis(lang='de'))
+    13818
+
+    """
+    return Wordnet(lang=lang, lexicon=lexicon).ilis(status=status)
diff --git a/wn/_db.py b/wn/_db.py
index d66183b..8acf296 100644
--- a/wn/_db.py
+++ b/wn/_db.py
@@ -11,7 +11,6 @@
 import wn
 from wn._types import Metadata, AnyPath
 from wn._util import resources, short_hash
-from wn import constants
 from wn import lmf
 
 
@@ -37,7 +36,7 @@
 # >>> wn._db.schema_hash(conn)
 #
 COMPATIBLE_SCHEMA_HASHES = {
-    '0cbec124b988d08e428b80d2b749563c2dccfa65',
+    '4c8ad03af5422d6979039ee2b80838d07c12d2c8',
 }
 
 
@@ -83,33 +82,19 @@ def connect() -> sqlite3.Connection:
             conn.set_trace_callback(print)
         if not initialized:
             logger.info('initializing database: %s', dbpath)
-            _initialize(conn)
+            _init_db(conn)
         _check_schema_compatibility(conn, dbpath)
 
         pool[dbpath] = conn
     return pool[dbpath]
 
 
-def _initialize(conn: sqlite3.Connection) -> None:
+def _init_db(conn: sqlite3.Connection) -> None:
     schema = resources.read_text('wn', 'schema.sql')
+    conn.executescript(schema)
     with conn:
-        conn.executescript(schema)
-        # prepare lookup tables
-        conn.executemany(
-            'INSERT INTO parts_of_speech (pos) VALUES (?)',
-            ((pos,) for pos in constants.PARTS_OF_SPEECH))
-        conn.executemany(
-            'INSERT INTO adjpositions (position) VALUES (?)',
-            ((adj,) for adj in constants.ADJPOSITIONS))
-        conn.executemany(
-            'INSERT INTO synset_relation_types (type) VALUES (?)',
-            ((typ,) for typ in constants.SYNSET_RELATIONS))
-        conn.executemany(
-            'INSERT INTO sense_relation_types (type) VALUES (?)',
-            ((typ,) for typ in constants.SENSE_RELATIONS))
-        conn.executemany(
-            'INSERT INTO lexicographer_files (id, name) VALUES (?,?)',
-            ((id, name) for name, id in constants.LEXICOGRAPHER_FILES.items()))
+        conn.executemany('INSERT INTO ili_statuses VALUES (null,?)',
+                         [('presupposed',), ('proposed',)])
 
 
 def _check_schema_compatibility(conn: sqlite3.Connection, dbpath: Path) -> None:
@@ -119,6 +104,9 @@ def _check_schema_compatibility(conn: sqlite3.Connection, dbpath: Path) -> None:
     if hash in COMPATIBLE_SCHEMA_HASHES:
         return
 
+    logger.debug('current schema hash:\n  %s', hash)
+    logger.debug('compatible schema hashes:\n  %s',
+                 '\n  '.join(COMPATIBLE_SCHEMA_HASHES))
     # otherwise, try to raise a helpful error message
     msg = ("Wn's schema has changed and is no longer compatible with the "
            f"database. Please move or delete {dbpath} and rebuild it.")
diff --git a/wn/_download.py b/wn/_download.py
index e57d7c7..d0416c8 100644
--- a/wn/_download.py
+++ b/wn/_download.py
@@ -63,7 +63,7 @@ def download(
 
     if progress_handler is None:
         progress_handler = ProgressHandler
-    progress = progress_handler(message='Download', unit='bytes')
+    progress = progress_handler(message='Download', unit=' bytes')
 
     try:
         if path.exists():
diff --git a/wn/_exceptions.py b/wn/_exceptions.py
index daac861..cff9aeb 100644
--- a/wn/_exceptions.py
+++ b/wn/_exceptions.py
@@ -4,3 +4,10 @@ class Error(Exception):
 
     # reset the module so the user sees the public name
     __module__ = 'wn'
+
+
+class WnWarning(Warning):
+    """Generic warning class for dubious worndet operations."""
+
+    # reset the module so the user sees the public name
+    __module__ = 'wn'
diff --git a/wn/_export.py b/wn/_export.py
index 43bbfc4..9c58773 100644
--- a/wn/_export.py
+++ b/wn/_export.py
@@ -1,5 +1,5 @@
 
-from typing import List, Set, Sequence
+from typing import List, Set, Sequence, Optional
 
 import wn
 from wn._types import AnyPath
@@ -8,14 +8,22 @@
     find_entries,
     find_senses,
     find_synsets,
+    find_syntactic_behaviours,
+    find_proposed_ilis,
     get_entry_senses,
     get_sense_relations,
     get_sense_synset_relations,
     get_synset_relations,
+    get_synset_members,
     get_examples,
     get_definitions,
     get_metadata,
     get_lexicalized,
+    get_adjposition,
+    get_form_pronunciations,
+    get_form_tags,
+    get_sense_counts,
+    get_lexfile,
 )
 from wn._core import Lexicon
 
@@ -70,6 +78,7 @@ def _export_lexicon(lexicon: Lexicon) -> lmf.Lexicon:
         version=lexicon.version,
         url=lexicon.url or '',
         citation=lexicon.citation or '',
+        logo=lexicon.logo or '',
         lexical_entries=_export_lexical_entries(lexids),
         synsets=_export_synsets(lexids),
         syntactic_behaviours=_export_syntactic_behaviours(lexids),
@@ -84,10 +93,20 @@ def _export_lexical_entries(lexids: Sequence[int]) -> List[lmf.LexicalEntry]:
     return [
         LexicalEntry(
             id,
-            Lemma(forms[0][0], pos, script=forms[0][1] or ''),  # TODO: tags
-            forms=[Form(form, script or '')
-                   for form, script, _ in forms[1:]],  # TODO: tags
-            senses=_export_senses(rowid),
+            Lemma(
+                forms[0][0],
+                pos,
+                script=forms[0][2] or '',
+                pronunciations=_export_pronunciations(forms[0][3]),
+                tags=_export_tags(forms[0][3])
+            ),
+            forms=[Form(fid,
+                        form,
+                        script or '',
+                        pronunciations=_export_pronunciations(frowid),
+                        tags=_export_tags(frowid))
+                   for form, fid, script, frowid in forms[1:]],
+            senses=_export_senses(rowid, lexids),
             meta=_export_metadata(rowid, 'entries'),
         )
         for id, pos, forms, _, rowid
@@ -95,25 +114,38 @@ def _export_lexical_entries(lexids: Sequence[int]) -> List[lmf.LexicalEntry]:
     ]
 
 
-def _export_senses(entry_rowid: int) -> List[lmf.Sense]:
+def _export_pronunciations(rowid: int) -> List[lmf.Pronunciation]:
+    Pron = lmf.Pronunciation
+    return [Pron(*data) for data in get_form_pronunciations(rowid)]
+
+
+def _export_tags(rowid: int) -> List[lmf.Tag]:
+    Tag = lmf.Tag
+    return [Tag(text, category) for text, category in get_form_tags(rowid)]
+
+
+def _export_senses(entry_rowid: int, lexids: Sequence[int]) -> List[lmf.Sense]:
     Sense = lmf.Sense
     return [
         Sense(
             id,
             synset,
-            relations=_export_sense_relations(rowid),
-            examples=_export_examples(rowid, 'senses'),
-            # TODO: counts
+            relations=_export_sense_relations(rowid, lexids),
+            examples=_export_examples(rowid, 'senses', lexids),
+            counts=_export_counts(rowid, lexids),
             lexicalized=get_lexicalized(rowid, 'senses'),
-            # TODO: adjposition
+            adjposition=get_adjposition(rowid) or '',
             meta=_export_metadata(rowid, 'senses'),
         )
         for id, _, synset, _, rowid
-        in get_entry_senses(entry_rowid)
+        in get_entry_senses(entry_rowid, lexids)
     ]
 
 
-def _export_sense_relations(sense_rowid: int) -> List[lmf.SenseRelation]:
+def _export_sense_relations(
+    sense_rowid: int,
+    lexids: Sequence[int]
+) -> List[lmf.SenseRelation]:
     SenseRelation = lmf.SenseRelation
     relations = [
         SenseRelation(
@@ -122,7 +154,7 @@ def _export_sense_relations(sense_rowid: int) -> List[lmf.SenseRelation]:
             meta=_export_metadata(rowid, 'sense_relations')
         )
         for type, rowid, id, *_
-        in get_sense_relations(sense_rowid, '*')
+        in get_sense_relations(sense_rowid, '*', lexids)
     ]
     relations.extend(
         SenseRelation(
@@ -131,12 +163,14 @@ def _export_sense_relations(sense_rowid: int) -> List[lmf.SenseRelation]:
             meta=_export_metadata(rowid, 'sense_synset_relations')
         )
         for type, rowid, id, *_
-        in get_sense_synset_relations(sense_rowid, '*')
+        in get_sense_synset_relations(sense_rowid, '*', lexids)
     )
     return relations
 
 
-def _export_examples(rowid: int, table: str) -> List[lmf.Example]:
+def _export_examples(
+    rowid: int, table: str, lexids: Sequence[int]
+) -> List[lmf.Example]:
     Example = lmf.Example
     return [
         Example(
@@ -145,43 +179,73 @@ def _export_examples(rowid: int, table: str) -> List[lmf.Example]:
             meta=_export_metadata(rowid, f'{table[:-1]}_examples')
         )
         for text, language, rowid
-        in get_examples(rowid, table)
+        in get_examples(rowid, table, lexids)
+    ]
+
+
+def _export_counts(rowid: int, lexids: Sequence[int]) -> List[lmf.Count]:
+    Count = lmf.Count
+    return [
+        Count(val, meta=_export_metadata(id, 'counts'))
+        for val, id in get_sense_counts(rowid, lexids)
     ]
 
 
 def _export_synsets(lexids: Sequence[int]) -> List[lmf.Synset]:
     Synset = lmf.Synset
-    return [
-        Synset(
-            id,
-            ili or '',
-            pos,
-            definitions=_export_definitions(rowid),
-            # TODO: ili_definition,
-            relations=_export_synset_relations(rowid),
-            examples=_export_examples(rowid, 'synsets'),
-            lexicalized=get_lexicalized(rowid, 'synsets'),
-            meta=_export_metadata(rowid, 'synsets'),
+    synsets = []
+    for id, pos, ili, _, rowid in find_synsets(lexicon_rowids=lexids):
+        ilidef = _export_ili_definition(rowid)
+        if ilidef and not ili:
+            ili = 'in'  # special case for proposed ILIs
+        synsets.append(
+            Synset(
+                id,
+                ili or '',
+                pos,
+                definitions=_export_definitions(rowid, lexids),
+                ili_definition=ilidef,
+                relations=_export_synset_relations(rowid, lexids),
+                examples=_export_examples(rowid, 'synsets', lexids),
+                lexicalized=get_lexicalized(rowid, 'synsets'),
+                members=[row[0] for row in get_synset_members(rowid, lexids)],
+                lexfile=get_lexfile(rowid),
+                meta=_export_metadata(rowid, 'synsets'),
+            )
         )
-        for id, pos, ili, _, rowid
-        in find_synsets(lexicon_rowids=lexids)
-    ]
+    return synsets
 
 
-def _export_definitions(rowid: int) -> List[lmf.Definition]:
+def _export_definitions(rowid: int, lexids: Sequence[int]) -> List[lmf.Definition]:
     Definition = lmf.Definition
     return [
         Definition(
             text,
             language,
+            source_sense=sense_id,
             meta=_export_metadata(rowid, 'definitions')
         )
-        for text, language, rowid
-        in get_definitions(rowid)
+        for text, language, sense_id, rowid
+        in get_definitions(rowid, lexids)
     ]
 
 
-def _export_synset_relations(synset_rowid: int) -> List[lmf.SynsetRelation]:
+def _export_ili_definition(synset_rowid: int) -> Optional[lmf.ILIDefinition]:
+    _, _, defn, rowid = next(find_proposed_ilis(synset_rowid=synset_rowid),
+                             (None, None, None, None))
+    ilidef = None
+    if defn:
+        meta = None
+        if rowid is not None:
+            meta = _export_metadata(rowid, 'proposed_ilis')
+        ilidef = lmf.ILIDefinition(defn or '', meta=meta)
+    return ilidef
+
+
+def _export_synset_relations(
+    synset_rowid: int,
+    lexids: Sequence[int]
+) -> List[lmf.SynsetRelation]:
     SynsetRelation = lmf.SynsetRelation
     relations = [
         SynsetRelation(
@@ -190,13 +254,17 @@ def _export_synset_relations(synset_rowid: int) -> List[lmf.SynsetRelation]:
             meta=_export_metadata(rowid, 'synset_relations')
         )
         for type, rowid, id, *_
-        in get_synset_relations((synset_rowid,), '*')
+        in get_synset_relations((synset_rowid,), '*', lexids)
     ]
     return relations
 
 
 def _export_syntactic_behaviours(lexids: Sequence[int]) -> List[lmf.SyntacticBehaviour]:
-    return []  # TODO
+    SyntacticBehaviour = lmf.SyntacticBehaviour
+    return [
+        SyntacticBehaviour(id, frame, senses)
+        for id, frame, senses in find_syntactic_behaviours(lexicon_rowids=lexids)
+    ]
 
 
 def _export_metadata(rowid: int, table: str) -> lmf.Metadata:
diff --git a/wn/_ili.py b/wn/_ili.py
new file mode 100644
index 0000000..9c81098
--- /dev/null
+++ b/wn/_ili.py
@@ -0,0 +1,37 @@
+
+from typing import Iterator, Dict
+from pathlib import Path
+
+from wn._types import AnyPath
+
+
+def is_ili(source: AnyPath) -> bool:
+    """Return True if *source* is an ILI tab-separated-value file.
+
+    This only checks that the first column, split by tabs, of the
+    first line is 'ili' or 'ILI'. It does not check if each line has
+    the correct number of columns.
+
+    """
+    source = Path(source).expanduser()
+    if source.is_file():
+        try:
+            with source.open('rb') as fh:
+                return next(fh).split(b'\t')[0] in (b'ili', b'ILI')
+        except (StopIteration, IndexError):
+            pass
+    return False
+
+
+def load(source: AnyPath) -> Iterator[Dict[str, str]]:
+    """Load an interlingual index file.
+
+    Args:
+        source: path to an ILI file
+    """
+    source = Path(source).expanduser()
+    with source.open(encoding='utf-8') as fh:
+        header = next(fh).rstrip('\r\n')
+        fields = tuple(map(str.lower, header.split('\t')))
+        for line in fh:
+            yield dict(zip(fields, line.rstrip('\r\n').split('\t')))
diff --git a/wn/_meta.py b/wn/_meta.py
index fb8cecd..1f199f1 100644
--- a/wn/_meta.py
+++ b/wn/_meta.py
@@ -1,2 +1,2 @@
 
-__version__ = '0.5.1'
+__version__ = '0.6.0'
diff --git a/wn/_queries.py b/wn/_queries.py
index d448b90..bc9b254 100644
--- a/wn/_queries.py
+++ b/wn/_queries.py
@@ -15,8 +15,17 @@
 
 # Local Types
 
+_Pronunciation = Tuple[
+    str,   # value
+    str,   # variety
+    str,   # notation
+    bool,  # phonemic
+    str,   # audio
+]
+_Tag = Tuple[str, str]  # tag, category
 _Form = Tuple[
     str,            # form
+    Optional[str],  # id
     Optional[str],  # script
     int             # rowid
 ]
@@ -43,6 +52,18 @@
     int,  # rowid
 ]
 _Sense_Relation = Tuple[str, int, str, str, str, int, int]  # relname, relid,  *_Sense
+_Count = Tuple[int, int]  # count, count_id
+_SyntacticBehaviour = Tuple[
+    str,       # id
+    str,       # frame
+    List[str]  # sense ids
+]
+_ILI = Tuple[
+    Optional[str],  # id
+    str,            # status
+    Optional[str],  # definition
+    int,            # rowid
+]
 _Lexicon = Tuple[
     int,       # rowid
     str,       # id
@@ -53,6 +74,7 @@
     str,       # version
     str,       # url
     str,       # citation
+    str,       # logo
     Metadata,  # metadata
 ]
 
@@ -83,7 +105,7 @@ def get_lexicon(rowid: int) -> _Lexicon:
 def _get_lexicon(conn: sqlite3.Connection, rowid: int) -> _Lexicon:
     query = '''
         SELECT DISTINCT rowid, id, label, language, email, license,
-                        version, url, citation
+                        version, url, citation, logo
         FROM lexicons
         WHERE rowid = ?
     '''
@@ -140,6 +162,123 @@ def _get_lexicon_rowids_for_lexicon(
     return lex_match
 
 
+def get_modified(rowid: int) -> bool:
+    query = 'SELECT modified FROM lexicons WHERE rowid = ?'
+    return connect().execute(query, (rowid,)).fetchone()[0]
+
+
+def get_lexicon_dependencies(rowid: int) -> List[Tuple[str, str, str, Optional[int]]]:
+    query = '''
+        SELECT provider_id, provider_version, provider_url, provider_rowid
+          FROM lexicon_dependencies
+         WHERE dependent_rowid = ?
+    '''
+    return connect().execute(query, (rowid,)).fetchall()
+
+
+def get_lexicon_extension_bases(rowid: int, depth: int = -1) -> List[int]:
+    query = '''
+          WITH RECURSIVE ext(x, d) AS
+               (SELECT base_rowid, 1
+                  FROM lexicon_extensions
+                 WHERE extension_rowid = :rowid
+                 UNION SELECT base_rowid, d+1
+                         FROM lexicon_extensions
+                         JOIN ext ON extension_rowid = x)
+        SELECT x FROM ext
+         WHERE :depth < 0 OR d <= :depth
+         ORDER BY d
+    '''
+    rows = connect().execute(query, {'rowid': rowid, 'depth': depth})
+    return [row[0] for row in rows]
+
+
+def get_lexicon_extensions(rowid: int, depth: int = -1) -> List[int]:
+    query = '''
+          WITH RECURSIVE ext(x, d) AS
+               (SELECT extension_rowid, 1
+                  FROM lexicon_extensions
+                 WHERE base_rowid = :rowid
+                 UNION SELECT extension_rowid, d+1
+                         FROM lexicon_extensions
+                         JOIN ext ON base_rowid = x)
+        SELECT x FROM ext
+         WHERE :depth < 0 OR d <= :depth
+         ORDER BY d
+    '''
+    rows = connect().execute(query, {'rowid': rowid, 'depth': depth})
+    return [row[0] for row in rows]
+
+
+def find_ilis(
+    id: str = None,
+    status: str = None,
+    lexicon_rowids: Sequence[int] = None,
+) -> Iterator[_ILI]:
+    if status != 'proposed':
+        yield from _find_existing_ilis(
+            id=id, status=status, lexicon_rowids=lexicon_rowids
+        )
+    if not id and (not status or status == 'proposed'):
+        yield from find_proposed_ilis(lexicon_rowids=lexicon_rowids)
+
+
+def _find_existing_ilis(
+    id: str = None,
+    status: str = None,
+    lexicon_rowids: Sequence[int] = None,
+) -> Iterator[_ILI]:
+    query = '''
+        SELECT DISTINCT i.id, ist.status, i.definition, i.rowid
+          FROM ilis AS i
+          JOIN ili_statuses AS ist ON i.status_rowid = ist.rowid
+    '''
+    conditions: List[str] = []
+    params: List = []
+    if id:
+        conditions.append('i.id = ?')
+        params.append(id)
+    if status:
+        conditions.append('ist.status = ?')
+        params.append(status)
+    if lexicon_rowids:
+        conditions.append(f'''
+            i.rowid IN
+            (SELECT ss.ili_rowid
+               FROM synsets AS ss
+              WHERE ss.lexicon_rowid IN ({_qs(lexicon_rowids)}))
+        ''')
+        params.extend(lexicon_rowids)
+    if conditions:
+        query += '\n WHERE ' + '\n   AND '.join(conditions)
+    yield from connect().execute(query, params)
+
+
+def find_proposed_ilis(
+    synset_rowid: int = None,
+    lexicon_rowids: Sequence[int] = None,
+) -> Iterator[_ILI]:
+    query = '''
+        SELECT null, "proposed", definition, rowid
+          FROM proposed_ilis
+    '''
+    conditions = []
+    params = []
+    if synset_rowid is not None:
+        conditions.append('synset_rowid = ?')
+        params.append(synset_rowid)
+    if lexicon_rowids:
+        conditions.append(f'''
+            synset_rowid IN
+            (SELECT ss.rowid FROM synsets AS ss
+              WHERE ss.lexicon_rowid IN ({_qs(lexicon_rowids)}))
+        ''')
+        params.extend(lexicon_rowids)
+    if conditions:
+        query += '\n WHERE ' + '\n   AND '.join(conditions)
+    yield from connect().execute(query, params)
+
+
 def find_entries(
         id: str = None,
         form: str = None,
@@ -148,10 +287,9 @@ def find_entries(
 ) -> Iterator[_Word]:
     conn = connect()
     query_parts = [
-        'SELECT DISTINCT e.lexicon_rowid, e.rowid, e.id, p.pos,'
-        '                f.form, f.script, f.rowid',
+        'SELECT DISTINCT e.lexicon_rowid, e.rowid, e.id, e.pos,'
+        '                f.form, f.id, f.script, f.rowid',
         '  FROM entries AS e',
-        '  JOIN parts_of_speech AS p ON p.rowid = e.pos_rowid',
         '  JOIN forms AS f ON f.entry_rowid = e.rowid',
     ]
 
@@ -163,7 +301,7 @@ def find_entries(
         conditions.append('e.rowid IN'
                           ' (SELECT entry_rowid FROM forms WHERE form = :form)')
     if pos:
-        conditions.append('p.pos = :pos')
+        conditions.append('e.pos = :pos')
     if lexicon_rowids:
         kws = {f'lex{i}': rowid for i, rowid in enumerate(lexicon_rowids, 1)}
         params.update(kws)
@@ -176,12 +314,12 @@ def find_entries(
 
     query = '\n'.join(query_parts)
     rows: Iterator[
-        Tuple[int, int, str, str, str, Optional[str], int]
+        Tuple[int, int, str, str, str, Optional[str], Optional[str], int]
     ] = conn.execute(query, params)
     groupby = itertools.groupby
     for key, group in groupby(rows, lambda row: row[0:4]):
         lexid, rowid, id, pos = key
-        forms = [(row[4], row[5], row[6]) for row in group]
+        forms = [(row[4], row[5], row[6], row[7]) for row in group]
         yield (id, pos, forms, lexid, rowid)
 
 
@@ -207,10 +345,7 @@ def find_senses(
         conditions.append('s.entry_rowid IN'
                           ' (SELECT entry_rowid FROM forms WHERE form = :form)')
     if pos:
-        conditions.append('e.pos_rowid IN'
-                          ' (SELECT p.rowid'
-                          '    FROM parts_of_speech AS p'
-                          '   WHERE p.pos = :pos)')
+        conditions.append('e.pos = :pos')
     if lexicon_rowids:
         kws = {f'lex{i}': rowid for i, rowid in enumerate(lexicon_rowids, 1)}
         params.update(kws)
@@ -233,9 +368,10 @@ def find_synsets(
 ) -> Iterator[_Synset]:
     conn = connect()
     query_parts = [
-        'SELECT DISTINCT ss.id, p.pos, ss.ili, ss.lexicon_rowid, ss.rowid',
+        'SELECT DISTINCT ss.id, ss.pos,',
+        '                (SELECT ilis.id FROM ilis WHERE ilis.rowid=ss.ili_rowid),',
+        '                ss.lexicon_rowid, ss.rowid',
         '  FROM synsets AS ss',
-        '  JOIN parts_of_speech AS p ON p.rowid = ss.pos_rowid',
     ]
 
     params: Dict[str, Any] = {'id': id, 'form': form, 'pos': pos, 'ili': ili}
@@ -243,18 +379,21 @@ def find_synsets(
     if id:
         conditions.append('ss.id = :id')
     if form:
-        query_parts.append(
-            '  JOIN (SELECT _s.synset_rowid, _s.entry_rowid, _s.entry_rank'
-            '          FROM senses AS _s'
-            '          JOIN forms AS f'
-            '            ON f.entry_rowid = _s.entry_rowid'
-            '         WHERE f.form = :form) AS s'
-            '    ON s.synset_rowid = ss.rowid'
-        )
+        query_parts.extend([
+            '  JOIN (SELECT _s.synset_rowid, _s.entry_rowid, _s.entry_rank',
+            '          FROM senses AS _s',
+            '          JOIN forms AS f',
+            '            ON f.entry_rowid = _s.entry_rowid',
+            '         WHERE f.form = :form) AS s',
+            '    ON s.synset_rowid = ss.rowid',
+        ])
     if pos:
-        conditions.append('p.pos = :pos')
+        conditions.append('ss.pos = :pos')
     if ili:
-        conditions.append('ss.ili = :ili')
+        query_parts.extend([
+            '  JOIN (SELECT _i.rowid FROM ilis AS _i WHERE _i.id=:ili) AS ili',
+            '    ON ss.ili_rowid = ili.rowid',
+        ])
     if lexicon_rowids:
         kws = {f'lex{i}': rowid for i, rowid in enumerate(lexicon_rowids, 1)}
         params.update(kws)
@@ -277,11 +416,10 @@ def get_synsets_for_ilis(
 ) -> Iterator[_Synset]:
     conn = connect()
     query = f'''
-        SELECT DISTINCT ss.id, p.pos, ss.ili, ss.lexicon_rowid, ss.rowid
+        SELECT DISTINCT ss.id, ss.pos, ili.id, ss.lexicon_rowid, ss.rowid
           FROM synsets as ss
-          JOIN parts_of_speech AS p
-            ON p.rowid = ss.pos_rowid
-         WHERE ss.ili IN ({_qs(ilis)})
+          JOIN ilis as ili ON ss.ili_rowid = ili.rowid
+         WHERE ili.id IN ({_qs(ilis)})
            AND ss.lexicon_rowid IN ({_qs(lexicon_rowids)})
     '''
     params = *ilis, *lexicon_rowids
@@ -290,45 +428,52 @@ def get_synsets_for_ilis(
 
 
 def get_synset_relations(
-        source_rowids: Collection[int],
-        relation_types: Collection[str],
+    source_rowids: Collection[int],
+    relation_types: Collection[str],
+    lexicon_rowids: Sequence[int],
 ) -> Iterator[_Synset_Relation]:
     conn = connect()
     params: List = []
     constraint = ''
-    if '*' not in relation_types:
-        constraint = f'WHERE t.type IN ({_qs(relation_types)})'
+    if relation_types and '*' not in relation_types:
+        constraint = f'WHERE type IN ({_qs(relation_types)})'
         params.extend(relation_types)
     params.extend(source_rowids)
+    params.extend(lexicon_rowids)
     query = f'''
-          WITH relation_types(type_rowid, type) AS  -- relation type lookup
-               (SELECT t.rowid, t.type
-                  FROM synset_relation_types AS t
-                {constraint})
-        SELECT DISTINCT rel.type, rel.rowid,
-                        tgt.id, p.pos, tgt.ili,
+          WITH rt(rowid, type) AS
+               (SELECT rowid, type FROM relation_types {constraint})
+        SELECT DISTINCT rel.type, rel.rowid, tgt.id, tgt.pos,
+                        (SELECT ilis.id FROM ilis WHERE ilis.rowid = tgt.ili_rowid),
                         tgt.lexicon_rowid, tgt.rowid
-          FROM (SELECT type, target_rowid, srel.rowid
+          FROM (SELECT rt.type, target_rowid, srel.rowid
                   FROM synset_relations AS srel
-                  JOIN relation_types USING (type_rowid)
-                 WHERE source_rowid IN ({_qs(source_rowids)})) AS rel
+                  JOIN rt ON srel.type_rowid = rt.rowid
+                 WHERE source_rowid IN ({_qs(source_rowids)})
+                   AND lexicon_rowid IN ({_qs(lexicon_rowids)})
+               ) AS rel
           JOIN synsets AS tgt
             ON tgt.rowid = rel.target_rowid
-          JOIN parts_of_speech AS p
-            ON p.rowid = tgt.pos_rowid
     '''
     result_rows: Iterator[_Synset_Relation] = conn.execute(query, params)
     yield from result_rows
 
 
-def get_definitions(synset_rowid: int) -> List[Tuple[str, str, int]]:
+def get_definitions(
+    synset_rowid: int,
+    lexicon_rowids: Sequence[int],
+) -> List[Tuple[str, str, str, int]]:
     conn = connect()
-    query = '''
-        SELECT definition, language, rowid
-          FROM definitions
-         WHERE synset_rowid = ?
+    query = f'''
+        SELECT d.definition,
+               d.language,
+               (SELECT s.id FROM senses AS s WHERE s.rowid=d.sense_rowid),
+               d.rowid
+          FROM definitions AS d
+         WHERE d.synset_rowid = ?
+           AND d.lexicon_rowid IN ({_qs(lexicon_rowids)})
     '''
-    return conn.execute(query, (synset_rowid,)).fetchall()
+    return conn.execute(query, (synset_rowid, *lexicon_rowids)).fetchall()
 
 
 _SANITIZED_EXAMPLE_PREFIXES = {
@@ -337,7 +482,11 @@ def get_definitions(synset_rowid: int) -> List[Tuple[str, str, int]]:
 }
 
 
-def get_examples(rowid: int, table: str) -> List[Tuple[str, str, int]]:
+def get_examples(
+    rowid: int,
+    table: str,
+    lexicon_rowids: Sequence[int],
+) -> List[Tuple[str, str, int]]:
     conn = connect()
     prefix = _SANITIZED_EXAMPLE_PREFIXES.get(table)
     if prefix is None:
@@ -346,11 +495,62 @@ def get_examples(rowid: int, table: str) -> List[Tuple[str, str, int]]:
         SELECT example, language, rowid
           FROM {prefix}_examples
          WHERE {prefix}_rowid = ?
+           AND lexicon_rowid IN ({_qs(lexicon_rowids)})
     '''
-    return conn.execute(query, (rowid,)).fetchall()
+    return conn.execute(query, (rowid, *lexicon_rowids)).fetchall()
 
 
-def _get_senses(rowid: int, sourcetype: str) -> Iterator[_Sense]:
+def find_syntactic_behaviours(
+    id: str = None,
+    lexicon_rowids: Sequence[int] = None,
+) -> Iterator[_SyntacticBehaviour]:
+    conn = connect()
+    query = '''
+        SELECT sb.id, sb.frame, s.id
+          FROM syntactic_behaviours AS sb
+          JOIN syntactic_behaviour_senses AS sbs
+            ON sbs.syntactic_behaviour_rowid = sb.rowid
+          JOIN senses AS s
+            ON s.rowid = sbs.sense_rowid
+    '''
+    conditions: List[str] = []
+    params: List = []
+    if id:
+        conditions.append('sb.id = ?')
+        params.append(id)
+    if lexicon_rowids:
+        conditions.append(f'sb.lexicon_rowid IN ({_qs(lexicon_rowids)})')
+        params.extend(lexicon_rowids)
+    if conditions:
+        query += '\n WHERE ' + '\n   AND '.join(conditions)
+    rows: Iterator[Tuple[str, str, str]] = conn.execute(query, params)
+    for key, group in itertools.groupby(rows, lambda row: row[0:2]):
+        id, frame = key
+        sense_ids = [row[2] for row in group]
+        yield id, frame, sense_ids
+
+
+def get_syntactic_behaviours(
+    rowid: int,
+    lexicon_rowids: Sequence[int],
+) -> List[str]:
+    conn = connect()
+    query = f'''
+        SELECT sb.frame
+          FROM syntactic_behaviours AS sb
+          JOIN syntactic_behaviour_senses AS sbs
+            ON sbs.syntactic_behaviour_rowid = sb.rowid
+         WHERE sbs.sense_rowid = ?
+           AND sb.lexicon_rowid IN ({_qs(lexicon_rowids)})
+    '''
+    return [row[0] for row in conn.execute(query, (rowid, *lexicon_rowids))]
+
+
+def _get_senses(
+    rowid: int,
+    sourcetype: str,
+    lexicon_rowids: Sequence[int],
+) -> Iterator[_Sense]:
     conn = connect()
     query = f'''
         SELECT s.id, e.id, ss.id, s.lexicon_rowid, s.rowid
@@ -360,41 +560,44 @@ def _get_senses(rowid: int, sourcetype: str) -> Iterator[_Sense]:
           JOIN synsets AS ss
             ON ss.rowid = s.synset_rowid
          WHERE s.{sourcetype}_rowid = ?
+           AND s.lexicon_rowid IN ({_qs(lexicon_rowids)})
+         ORDER BY s.{sourcetype}_rank
     '''
-    return conn.execute(query, (rowid,))
+    return conn.execute(query, (rowid, *lexicon_rowids))
 
 
-def get_entry_senses(rowid: int) -> Iterator[_Sense]:
-    yield from _get_senses(rowid, 'entry')
+def get_entry_senses(rowid: int, lexicon_rowids: Sequence[int]) -> Iterator[_Sense]:
+    yield from _get_senses(rowid, 'entry', lexicon_rowids)
 
 
-def get_synset_members(rowid: int) -> Iterator[_Sense]:
-    yield from _get_senses(rowid, 'synset')
+def get_synset_members(rowid: int, lexicon_rowids: Sequence[int]) -> Iterator[_Sense]:
+    yield from _get_senses(rowid, 'synset', lexicon_rowids)
 
 
 def get_sense_relations(
-        source_rowid: int,
-        relation_types: Collection[str],
+    source_rowid: int,
+    relation_types: Collection[str],
+    lexicon_rowids: Sequence[int],
 ) -> Iterator[_Sense_Relation]:
-    conn = connect()
     params: List = []
     constraint = ''
     if '*' not in relation_types:
-        constraint = f'WHERE t.type IN ({_qs(relation_types)})'
+        constraint = f'WHERE type IN ({_qs(relation_types)})'
         params.extend(relation_types)
     params.append(source_rowid)
+    params.extend(lexicon_rowids)
     query = f'''
-          WITH relation_types(type_rowid, type) AS  -- relation type lookup
-               (SELECT t.rowid, t.type
-                  FROM sense_relation_types AS t
-                {constraint})
+          WITH rt(rowid, type) AS
+               (SELECT rowid, type FROM relation_types {constraint})
         SELECT DISTINCT rel.type, rel.rowid,
                         s.id, e.id, ss.id,
                         s.lexicon_rowid, s.rowid
-          FROM (SELECT type, target_rowid, srel.rowid
+          FROM (SELECT rt.type, target_rowid, srel.rowid
                   FROM sense_relations AS srel
-                  JOIN relation_types USING (type_rowid)
-                 WHERE source_rowid = ?) AS rel
+                  JOIN rt ON srel.type_rowid = rt.rowid
+                 WHERE source_rowid = ?
+                   AND lexicon_rowid IN ({_qs(lexicon_rowids)})
+               ) AS rel
           JOIN senses AS s
             ON s.rowid = rel.target_rowid
           JOIN entries AS e
@@ -402,42 +605,44 @@ def get_sense_relations(
           JOIN synsets AS ss
             ON ss.rowid = s.synset_rowid
     '''
-    rows: Iterator[_Sense_Relation] = conn.execute(query, params)
+    rows: Iterator[_Sense_Relation] = connect().execute(query, params)
     yield from rows
 
 
 def get_sense_synset_relations(
         source_rowid: int,
         relation_types: Collection[str],
+        lexicon_rowids: Sequence[int],
 ) -> Iterator[_Synset_Relation]:
-    conn = connect()
-    params: List = [source_rowid]
+    params: List = []
     constraint = ''
     if '*' not in relation_types:
-        constraint = f'WHERE t.type IN ({_qs(relation_types)})'
+        constraint = f'WHERE type IN ({_qs(relation_types)})'
         params.extend(relation_types)
+    params.append(source_rowid)
+    params.extend(lexicon_rowids)
     query = f'''
-          WITH relation_types(type_rowid, type) AS  -- relation type lookup
-               (SELECT t.rowid, t.type
-                  FROM sense_relation_types AS t
-                {constraint})
-        SELECT DISTINCT rel.type, rel.rowid,
-                        ss.id, p.pos, ss.ili,
+          WITH rt(rowid, type) AS
+               (SELECT rowid, type FROM relation_types {constraint})
+        SELECT DISTINCT rel.type, rel.rowid, ss.id, ss.pos,
+                        (SELECT ilis.id FROM ilis WHERE ilis.rowid = ss.ili_rowid),
                         ss.lexicon_rowid, ss.rowid
-          FROM (SELECT type, target_rowid, srel.rowid
+          FROM (SELECT rt.type, target_rowid, srel.rowid
                   FROM sense_synset_relations AS srel
-                  JOIN relation_types USING (type_rowid)
-                 WHERE source_rowid = ?) AS rel
+                  JOIN rt ON srel.type_rowid = rt.rowid
+                 WHERE source_rowid = ?
+                   AND lexicon_rowid IN ({_qs(lexicon_rowids)})
+               ) AS rel
           JOIN synsets AS ss
             ON ss.rowid = rel.target_rowid
-          JOIN parts_of_speech AS p
-            ON p.rowid = ss.pos_rowid
     '''
-    rows: Iterator[_Synset_Relation] = conn.execute(query, params)
+    rows: Iterator[_Synset_Relation] = connect().execute(query, params)
     yield from rows
 
 
 _SANITIZED_METADATA_TABLES = {
+    'ilis': 'ilis',
+    'proposed_ilis': 'proposed_ilis',
     'lexicons': 'lexicons',
     'entries': 'entries',
     'senses': 'senses',
@@ -446,6 +651,7 @@ def get_sense_synset_relations(
     'sense_synset_relations': 'sense_synset_relations',
     'synset_relations': 'synset_relations',
     'sense_examples': 'sense_examples',
+    'counts': 'counts',
     'synset_examples': 'synset_examples',
     'definitions': 'definitions',
 }
@@ -479,5 +685,62 @@ def get_lexicalized(rowid: int, table: str) -> bool:
     return conn.execute(query, (rowid,)).fetchone()[0]
 
 
+def get_adjposition(rowid: int) -> Optional[str]:
+    conn = connect()
+    query = 'SELECT adjposition FROM adjpositions WHERE sense_rowid = ?'
+    row = conn.execute(query, (rowid,)).fetchone()
+    if row:
+        return row[0]
+    return None
+
+
+def get_form_pronunciations(form_rowid: int) -> List[_Pronunciation]:
+    # TODO: restrict by lexicon ids
+    conn = connect()
+    query = '''
+        SELECT value, variety, notation, phonemic, audio
+          FROM pronunciations
+         WHERE form_rowid = ?
+    '''
+    rows: List[_Pronunciation] = conn.execute(query, (form_rowid,)).fetchall()
+    return rows
+
+
+def get_form_tags(form_rowid: int) -> List[_Tag]:
+    # TODO: restrict by lexicon ids
+    conn = connect()
+    query = 'SELECT tag, category FROM tags WHERE form_rowid = ?'
+    rows: List[_Tag] = conn.execute(query, (form_rowid,)).fetchall()
+    return rows
+
+
+def get_sense_counts(sense_rowid: int, lexicon_rowids: Sequence[int]) -> List[_Count]:
+    conn = connect()
+    query = f'''
+        SELECT count, rowid
+          FROM counts
+         WHERE sense_rowid = ?
+           AND lexicon_rowid IN ({_qs(lexicon_rowids)})
+    '''
+    rows: List[_Count] = conn.execute(
+        query, (sense_rowid, *lexicon_rowids)
+    ).fetchall()
+    return rows
+
+
+def get_lexfile(synset_rowid: int) -> Optional[str]:
+    conn = connect()
+    query = '''
+        SELECT lf.name
+          FROM lexfiles AS lf
+          JOIN synsets AS ss ON ss.lexfile_rowid = lf.rowid
+         WHERE ss.rowid = ?
+    '''
+    row = conn.execute(query, (synset_rowid,)).fetchone()
+    if row is not None and row[0] is not None:
+        return row[0]
+    return None
+
+
 def _qs(xs: Collection) -> str: return ','.join('?' * len(xs))
 def _kws(xs: Collection) -> str: return ','.join(f':{x}' for x in xs)
diff --git a/wn/_util.py b/wn/_util.py
index 2614f9c..147bec2 100644
--- a/wn/_util.py
+++ b/wn/_util.py
@@ -4,6 +4,7 @@
 import sys
 from pathlib import Path
 import hashlib
+from unicodedata import normalize, combining
 # version check is for mypy; see https://github.com/python/mypy/issues/1153
 if sys.version_info >= (3, 7):
     import importlib.resources as resources
@@ -52,3 +53,7 @@ def short_hash(string: str) -> str:
 
 def flatten(iterable: Iterable[Iterable[T]]) -> List[T]:
     return [x for xs in iterable for x in xs]
+
+
+def normalize_form(s: str) -> str:
+    return ''.join(c for c in normalize('NFKD', s.lower()) if not combining(c))
diff --git a/wn/constants.py b/wn/constants.py
index 3eb4db0..c4d9f41 100644
--- a/wn/constants.py
+++ b/wn/constants.py
@@ -2,9 +2,8 @@
 Constants and literals used in wordnets.
 """
 
-# Relation Types
 
-SENSE_RELATIONS = frozenset((
+SENSE_RELATIONS = frozenset([
     'antonym',
     'also',
     'participle',
@@ -18,9 +17,33 @@
     'is_exemplified_by',
     'similar',
     'other',
-))
+    'feminine',
+    'has_feminine',
+    'masculine',
+    'has_masculine',
+    'young',
+    'has_young',
+    'diminutive',
+    'has_diminutive',
+    'augmentative',
+    'has_augmentative',
+    'anto_gradable',
+    'anto_simple',
+    'anto_converse',
+    'simple_aspect_ip',
+    'secondary_aspect_ip',
+    'simple_aspect_pi',
+    'secondary_aspect_pi',
+])
 
-SYNSET_RELATIONS = frozenset((
+SENSE_SYNSET_RELATIONS = frozenset([
+    'other',
+    'domain_topic',
+    'domain_region',
+    'exemplifies',
+])
+
+SYNSET_RELATIONS = frozenset([
     'agent',
     'also',
     'attribute',
@@ -92,7 +115,21 @@
     'subevent',
     'is_subevent_of',
     'antonym',
-))
+    'feminine',
+    'has_feminine',
+    'masculine',
+    'has_masculine',
+    'young',
+    'has_young',
+    'diminutive',
+    'has_diminutive',
+    'augmentative',
+    'has_augmentative',
+    'anto_gradable',
+    'anto_simple',
+    'anto_converse',
+    'ir_synonym',
+])
 
 
 # Adjective Positions
@@ -181,3 +218,8 @@
     'verb.weather': 43,
     'adj.ppl': 44,
 }
+
+# resource types
+
+_WORDNET = 'wordnet'
+_ILI = 'ili'
diff --git a/wn/index.toml b/wn/index.toml
index a55c140..7c1de0c 100644
--- a/wn/index.toml
+++ b/wn/index.toml
@@ -1,21 +1,28 @@
+[cili]
+  type = "ili"
+  label = "Collaborative Interlingual Index"
+  license = "https://creativecommons.org/licenses/by/4.0/"
+  [cili.versions."1.0"]
+    url = "https://github.com/globalwordnet/cili/releases/download/v1.0/cili.tsv.xz"
+
 [ewn]
   label = "Open English WordNet"
   language = "en"
   license = "https://creativecommons.org/licenses/by/4.0/"
-  [ewn.versions.2019]
-    url = "https://en-word.net/static/english-wordnet-2019.xml.gz"
   [ewn.versions.2020]
     url = "https://en-word.net/static/english-wordnet-2020.xml.gz"
+  [ewn.versions.2019]
+    url = "https://en-word.net/static/english-wordnet-2019.xml.gz"
 
 
 [pwn]
   label = "Princeton WordNet"
   language = "en"
   license = "https://wordnet.princeton.edu/license-and-commercial-use"
-  [pwn.versions."3.0"]
-    url = "https://github.com/bond-lab/omw-data/releases/download/v1.3/pwn30.tar.xz"
   [pwn.versions."3.1"]
     url = "https://github.com/bond-lab/omw-data/releases/download/v1.3/pwn31.tar.xz"
+  [pwn.versions."3.0"]
+    url = "https://github.com/bond-lab/omw-data/releases/download/v1.3/pwn30.tar.xz"
 
 
 [odenet]
diff --git a/wn/lmf.py b/wn/lmf.py
index dfd8a7d..a1c098e 100644
--- a/wn/lmf.py
+++ b/wn/lmf.py
@@ -4,17 +4,15 @@
 """
 
 from typing import (
-    TypeVar,
-    Type,
     Container,
     List,
     Tuple,
     Dict,
-    Set,
     NamedTuple,
     Optional,
     TextIO,
-    BinaryIO
+    BinaryIO,
+    Iterator,
 )
 from pathlib import Path
 import warnings
@@ -27,9 +25,11 @@
 from wn._util import is_xml
 from wn.constants import (
     SENSE_RELATIONS,
+    SENSE_SYNSET_RELATIONS,
     SYNSET_RELATIONS,
     ADJPOSITIONS,
     PARTS_OF_SPEECH,
+    LEXICOGRAPHER_FILES,
 )
 
 
@@ -78,6 +78,45 @@ class LMFWarning(Warning):
 }
 
 
+class XMLEventIterator:
+    """etree.iterparse() event iterator with lookahead"""
+    def __init__(self, iterator: Iterator[Tuple[str, ET.Element]]):
+        self.iterator = iterator
+        self._next = next(iterator, (None, None))
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        _next = self._next
+        if _next == (None, None):
+            raise StopIteration
+        self._next = next(self.iterator, (None, None))
+        return _next
+
+    def starts(self, *tags: str) -> bool:
+        event, elem = self._next
+        if elem is None:
+            return False
+        return event == 'start' and elem.tag in tags
+
+    def start(self, *tags: str) -> ET.Element:
+        event, elem = next(self)
+        if event != 'start':
+            raise LMFError(f'expected <{"|".join(tags)}>, got </{elem.tag}>')
+        if elem.tag not in tags:
+            raise LMFError(f'expected <{"|".join(tags)}>, got <{elem.tag}>')
+        return elem
+
+    def end(self, *tags: str) -> ET.Element:
+        event, elem = next(self)
+        if event != 'end':
+            raise LMFError(f'expected </{"|".join(tags)}>, got <{elem.tag}>')
+        if elem.tag not in tags:
+            raise LMFError(f'expected </{"|".join(tags)}>, got </{elem.tag}>')
+        return elem
+
+
 class Metadata(NamedTuple):
     contributor: Optional[str] = None
     coverage: Optional[str] = None
@@ -117,9 +156,10 @@ def __init__(self, value: int, meta: Metadata = None):
 
 
 class SyntacticBehaviour:
-    __slots__ = 'frame', 'senses'
+    __slots__ = 'id', 'frame', 'senses'
 
-    def __init__(self, frame: str, senses: List[str] = None):
+    def __init__(self, id: str, frame: str, senses: List[str] = None):
+        self.id = id
         self.frame = frame
         self.senses = senses or []
 
@@ -176,19 +216,23 @@ def __init__(
 
 class Synset(_HasMeta):
     __slots__ = ('id', 'ili', 'pos', 'definitions', 'ili_definition',
-                 'relations', 'examples', 'lexicalized')
+                 'relations', 'examples', 'lexicalized', 'members',
+                 'lexfile', 'external')
 
     def __init__(
-            self,
-            id: str,
-            ili: str,
-            pos: str,
-            definitions: List[Definition] = None,
-            ili_definition: ILIDefinition = None,
-            relations: List[SynsetRelation] = None,
-            examples: List[Example] = None,
-            lexicalized: bool = True,
-            meta: Metadata = None):
+        self,
+        id: str,
+        ili: str = None,
+        pos: str = None,
+        definitions: List[Definition] = None,
+        ili_definition: ILIDefinition = None,
+        relations: List[SynsetRelation] = None,
+        examples: List[Example] = None,
+        lexicalized: bool = True,
+        members: List[str] = None,
+        lexfile: str = None,
+        meta: Metadata = None
+    ):
         super().__init__(meta)
         self.id = id
         self.ili = ili
@@ -198,22 +242,45 @@ def __init__(
         self.relations = relations or []
         self.examples = examples or []
         self.lexicalized = lexicalized
+        self.members = members or []
+        self.lexfile = lexfile
+        self.external = False
+
+    @classmethod
+    def as_external(
+        cls,
+        id: str,
+        definitions: List[Definition] = None,
+        relations: List[SynsetRelation] = None,
+        examples: List[Example] = None
+    ):
+        obj = cls(
+            id,
+            '',
+            '',
+            definitions=definitions,
+            relations=relations,
+            examples=examples
+        )
+        obj.external = True
+        return obj
 
 
 class Sense(_HasMeta):
     __slots__ = ('id', 'synset', 'relations', 'examples', 'counts',
-                 'lexicalized', 'adjposition')
+                 'lexicalized', 'adjposition', 'external')
 
     def __init__(
-            self,
-            id: str,
-            synset: str,
-            relations: List[SenseRelation] = None,
-            examples: List[Example] = None,
-            counts: List[Count] = None,
-            lexicalized: bool = True,
-            adjposition: str = '',
-            meta: Metadata = None):
+        self,
+        id: str,
+        synset: str,
+        relations: List[SenseRelation] = None,
+        examples: List[Example] = None,
+        counts: List[Count] = None,
+        lexicalized: bool = True,
+        adjposition: str = '',
+        meta: Metadata = None
+    ):
         super().__init__(meta)
         self.id = id
         self.synset = synset
@@ -222,6 +289,43 @@ def __init__(
         self.counts = counts or []
         self.lexicalized = lexicalized
         self.adjposition = adjposition
+        self.external = False
+
+    @classmethod
+    def as_external(
+        cls,
+        id: str,
+        relations: List[SenseRelation] = None,
+        examples: List[Example] = None,
+        counts: List[Count] = None
+    ):
+        obj = cls(
+            id,
+            '',
+            relations=relations,
+            examples=examples,
+            counts=counts
+        )
+        obj.external = True
+        return obj
+
+
+class Pronunciation:
+    __slots__ = 'value', 'variety', 'notation', 'phonemic', 'audio'
+
+    def __init__(
+        self,
+        value: str,
+        variety: str = None,
+        notation: str = None,
+        phonemic: bool = True,
+        audio: str = None,
+    ):
+        self.value = value
+        self.variety = variety
+        self.notation = notation
+        self.phonemic = phonemic
+        self.audio = audio
 
 
 class Tag:
@@ -233,45 +337,116 @@ def __init__(self, text: str, category: str):
 
 
 class Form:
-    __slots__ = 'form', 'script', 'tags'
+    __slots__ = 'id', 'form', 'script', 'pronunciations', 'tags', 'external'
 
-    def __init__(self, form: str, script: str, tags: List[Tag] = None):
+    def __init__(
+        self,
+        id: Optional[str],
+        form: str,
+        script: str,
+        pronunciations: List[Pronunciation] = None,
+        tags: List[Tag] = None,
+    ):
+        self.id = id
         self.form = form
         self.script = script
+        self.pronunciations = pronunciations or []
         self.tags = tags or []
+        self.external = False
+
+    @classmethod
+    def as_external(
+        cls,
+        id: str,
+        pronunciations: List[Pronunciation] = None,
+        tags: List[Tag] = None,
+    ):
+        obj = cls(
+            id,
+            '',
+            '',
+            pronunciations=pronunciations,
+            tags=tags,
+        )
+        obj.external = True
+        return obj
 
 
 class Lemma:
-    __slots__ = 'form', 'pos', 'script', 'tags'
+    __slots__ = 'form', 'pos', 'script', 'pronunciations', 'tags', 'external'
 
-    def __init__(self, form: str, pos: str, script: str = '', tags: List[Tag] = None):
+    def __init__(
+        self,
+        form: str,
+        pos: str,
+        script: str = '',
+        pronunciations: List[Pronunciation] = None,
+        tags: List[Tag] = None,
+    ):
         self.form = form
         self.pos = pos
         self.script = script
+        self.pronunciations = pronunciations or []
         self.tags = tags or []
+        self.external = False
+
+    @classmethod
+    def as_external(
+        cls,
+        pronunciations: List[Pronunciation] = None,
+        tags: List[Tag] = None,
+    ):
+        obj = cls(
+            '',
+            '',
+            pronunciations=pronunciations,
+            tags=tags,
+        )
+        obj.external = True
+        return obj
 
 
 class LexicalEntry(_HasMeta):
-    __slots__ = 'id', 'lemma', 'forms', 'senses'
+    __slots__ = 'id', 'lemma', 'forms', 'senses', 'external'
 
     def __init__(
-            self,
-            id: str,
-            lemma: Lemma,
-            forms: List[Form] = None,
-            senses: List[Sense] = None,
-            meta: Metadata = None):
+        self,
+        id: str,
+        lemma: Optional[Lemma],
+        forms: List[Form] = None,
+        senses: List[Sense] = None,
+        meta: Metadata = None
+    ):
         super().__init__(meta)
         self.id = id
         self.lemma = lemma
         self.forms = forms or []
         self.senses = senses or []
+        self.external = False
+
+    @classmethod
+    def as_external(
+        cls,
+        id: str,
+        lemma: Lemma = None,
+        forms: List[Form] = None,
+        senses: List[Sense] = None,
+    ):
+        obj = cls(
+            id,
+            lemma,
+            forms=forms,
+            senses=senses
+        )
+        obj.external = True
+        return obj
 
 
 class Lexicon(_HasMeta):
     __slots__ = ('id', 'label', 'language',
-                 'email', 'license', 'version', 'url', 'citation',
-                 'lexical_entries', 'synsets', 'syntactic_behaviours')
+                 'email', 'license', 'version', 'url', 'citation', 'logo',
+                 'lexical_entries', 'synsets', 'syntactic_behaviours',
+                 'extends', 'requires')
 
     def __init__(
             self,
@@ -283,9 +458,12 @@ def __init__(
             version: str,
             url: str = '',
             citation: str = '',
+            logo: str = '',
             lexical_entries: List[LexicalEntry] = None,
             synsets: List[Synset] = None,
             syntactic_behaviours: List[SyntacticBehaviour] = None,
+            extends: Dict[str, str] = None,
+            requires: List[Dict[str, str]] = None,
             meta: Metadata = None):
         super().__init__(meta)
         self.id = id
@@ -296,18 +474,12 @@ def __init__(
         self.version = version
         self.url = url
         self.citation = citation
+        self.logo = logo
         self.lexical_entries = lexical_entries or []
         self.synsets = synsets or []
         self.syntactic_behaviours = syntactic_behaviours or []
-
-    def entry_ids(self) -> Set[str]:
-        return {entry.id for entry in self.lexical_entries}
-
-    def sense_ids(self) -> Set[str]:
-        return {sense.id for entry in self.lexical_entries for sense in entry.senses}
-
-    def synset_ids(self) -> Set[str]:
-        return {synset.id for synset in self.synsets}
+        self.extends = extends
+        self.requires = requires or []
 
 
 LexicalResource = List[Lexicon]
@@ -347,15 +519,23 @@ def scan_lexicons(source: AnyPath) -> List[Dict]:
 
     source = Path(source).expanduser()
 
-    # this is implemeted with expat as it's much faster than etree for
+    # this is implemented with expat as it's much faster than etree for
     # this task
     infos = []
 
     def start(name, attrs):
-        if name == 'Lexicon':
+        if name in ('Lexicon', 'LexiconExtension'):
             attrs['counts'] = {}
+            attrs['relations'] = set()
+            attrs['lexfiles'] = set()
             infos.append(attrs)
+        elif name == 'Extends':
+            infos[-1]['extends'] = attrs['id'], attrs['version']
         elif infos:
+            if name in ('SynsetRelation', 'SenseRelation'):
+                infos[-1]['relations'].add(attrs['relType'])
+            elif name == 'Synset' and 'lexfile' in attrs:
+                infos[-1]['lexfiles'].add(attrs['lexfile'])
             counts = infos[-1]['counts']
             counts[name] = counts.get(name, 0) + 1
 
@@ -381,64 +561,47 @@ def load(source: AnyPath) -> LexicalResource:
     with source.open('rb') as fh:
         version = _read_header(fh)
 
-    events = ET.iterparse(source, events=('start', 'end'))
-    root = next(events)[1]
-    event, elem = next(events)
+    events = XMLEventIterator(ET.iterparse(source, events=('start', 'end')))
+    root = events.start('LexicalResource')
 
     lexicons: List[Lexicon] = []
-    while event == 'start' and elem.tag == 'Lexicon':
-        lexicons.append(_load_lexicon(elem, events, version))
+    while events.starts('Lexicon', 'LexiconExtension'):
+        lexicons.append(_load_lexicon(events, version))
         root.clear()
-        event, elem = next(events)
 
-    _assert_closed(event, elem, 'LexicalResource')
+    events.end(root.tag)
     list(events)  # consume remaining events, if any
 
     return lexicons
 
 
-def dump(
-        lexicons: LexicalResource, destination: AnyPath, version: str = '1.0'
-) -> None:
-    """Write wordnets in the WN-LMF format.
-
-    Args:
-        lexicons: a list of :class:`Lexicon` objects
-    """
-    destination = Path(destination).expanduser()
-    doctype = _DOCTYPE.format(schema=_SCHEMAS[version])
-    dc_uri = _DC_URIS[version]
-    with destination.open('wt', encoding='utf-8') as out:
-        print(_XMLDECL.decode('utf-8'), file=out)
-        print(doctype, file=out)
-        print(f'<LexicalResource xmlns:dc="{dc_uri}">', file=out)
-        for lexicon in lexicons:
-            _dump_lexicon(lexicon, out, version)
-        print('</LexicalResource>', file=out)
-
-
-def _load_lexicon(local_root, events, version) -> Lexicon:
-    attrs = local_root.attrib
-    event, elem = next(events)
-
-    syntactic_behaviours: List[SyntacticBehaviour] = []
-    lexical_entries: List[LexicalEntry] = []
-    while event == 'start' and elem.tag == 'LexicalEntry':
-        entry, sbs = _load_lexical_entry(elem, events, version)
-        lexical_entries.append(entry)
-        syntactic_behaviours.extend(sbs)
-        local_root.clear()
-        event, elem = next(events)
-
-    synsets: List[Synset] = []
-    while event == 'start' and elem.tag == 'Synset':
-        synsets.append(_load_synset(elem, events, version))
-        local_root.clear()
-        event, elem = next(events)
+def _load_lexicon(events, version) -> Lexicon:
+    extends: Optional[Dict[str, str]] = None
+    requires: List[Dict[str, str]] = []
 
-    _assert_closed(event, elem, 'Lexicon')
+    if version == '1.0':
+        lex_root = events.start('Lexicon')
+        extension = False
+    else:
+        lex_root = events.start('Lexicon', 'LexiconExtension')
+        extension = lex_root.tag == 'LexiconExtension'
+        extends = _load_dependency(events, 'Extends') if extension else None
+        while events.starts('Requires'):
+            requires.append(_load_dependency(events, 'Requires'))
+
+    attrs = lex_root.attrib
+    entries, frames, sbmap = _load_lexical_entries(
+        events, extension, version, lex_root
+    )
+    synsets = _load_synsets(
+        events, extension, version, lex_root
+    )
+    if version != '1.0':
+        frames.extend(_load_syntactic_behaviours(events, version))
+        for sb in frames:
+            sb.senses.extend(sbmap.get(sb.id, []))
 
-    return Lexicon(
+    lex = Lexicon(
         attrs['id'],
         attrs['label'],
         attrs['language'],
@@ -447,256 +610,340 @@ def _load_lexicon(local_root, events, version) -> Lexicon:
         attrs['version'],
         url=attrs.get('url'),
         citation=attrs.get('citation'),
-        lexical_entries=lexical_entries,
+        logo=attrs.get('logo'),
+        lexical_entries=entries,
         synsets=synsets,
-        syntactic_behaviours=syntactic_behaviours,
+        syntactic_behaviours=frames,
+        extends=extends,
+        requires=requires,
         meta=_get_metadata(attrs, version),
     )
+    events.end(lex_root.tag)
 
+    return lex
 
-def _load_lexical_entry(
-        local_root,
-        events,
-        version,
-) -> Tuple[LexicalEntry, List[SyntacticBehaviour]]:
-    attrs = local_root.attrib
-    lemma: Lemma = _load_lemma(events)
-    event, elem = next(events)
 
-    forms: List[Form] = []
-    while event == 'start' and elem.tag == 'Form':
-        forms.append(_load_form(elem, events))
-        event, elem = next(events)
+def _load_dependency(events, tag) -> Dict[str, str]:
+    events.start(tag)
+    elem = events.end(tag)
+    return {'id': elem.attrib['id'],
+            'version': elem.attrib['version'],
+            'url': elem.attrib.get('url')}
 
-    senses: List[Sense] = []
-    while event == 'start' and elem.tag == 'Sense':
-        senses.append(_load_sense(elem, events, version))
-        event, elem = next(events)
 
+def _load_lexical_entries(
+    events: XMLEventIterator,
+    extension: bool,
+    version: str,
+    lex_root: ET.Element,
+) -> Tuple[List[LexicalEntry], List[SyntacticBehaviour], Dict[str, List[str]]]:
+    entries: List[LexicalEntry] = []
     syntactic_behaviours: List[SyntacticBehaviour] = []
-    while event == 'start' and elem.tag == 'SyntacticBehaviour':
-        event, elem = next(events)
-        _assert_closed(event, elem, 'SyntacticBehaviour')
-        syntactic_behaviours.append(_load_syntactic_behaviour(elem))
-        event, elem = next(events)
-
-    _assert_closed(event, elem, 'LexicalEntry')
-
-    entry = LexicalEntry(
-        attrs['id'],
-        lemma,
-        forms=forms,
-        senses=senses,
-        meta=_get_metadata(attrs, version),
-    )
-
-    return entry, syntactic_behaviours
-
-
-def _load_lemma(events) -> Lemma:
-    event, elem = next(events)
-    if event != 'start' or elem.tag != 'Lemma':
-        raise LMFError('expected a Lemma element')
-    attrs = elem.attrib
-    return Lemma(
-        attrs['writtenForm'],
-        _get_literal(attrs['partOfSpeech'], PARTS_OF_SPEECH),
-        script=attrs.get('script'),
-        tags=_load_tags_until(events, 'Lemma'))
-
+    sbmap: Dict[str, List[str]] = {}
+    while True:
+        if events.starts('LexicalEntry'):
+            attrs = events.start('LexicalEntry').attrib
+            entry = LexicalEntry(
+                attrs['id'],
+                _load_lemma(events, False),
+                forms=_load_forms(events, False),
+                senses=_load_senses(events, sbmap, False, version),
+                meta=_get_metadata(attrs, version),
+            )
+            syntactic_behaviours.extend(_load_syntactic_behaviours(events, version))
+            events.end('LexicalEntry')
+        elif extension and events.starts('ExternalLexicalEntry'):
+            attrs = events.start('ExternalLexicalEntry').attrib
+            entry = LexicalEntry.as_external(
+                attrs['id'],
+                lemma=_load_lemma(events, True),
+                forms=_load_forms(events, True),
+                senses=_load_senses(events, sbmap, True, version)
+            )
+            syntactic_behaviours.extend(_load_syntactic_behaviours(events, version))
+            events.end('ExternalLexicalEntry')
+        else:
+            break
+        entries.append(entry)
+        lex_root.clear()
+
+    return entries, syntactic_behaviours, sbmap
+
+
+def _load_lemma(events, external) -> Optional[Lemma]:
+    lemma: Optional[Lemma] = None
+    if external and events.starts('ExternalLemma'):
+        next(events)
+        lemma = Lemma.as_external(
+            pronunciations=_load_pronunciations(events),
+            tags=_load_tags(events)
+        )
+        events.end('ExternalLemma')
+    elif not external or events.starts('Lemma'):
+        attrs = events.start('Lemma').attrib
+        lemma = Lemma(
+            attrs['writtenForm'],
+            _get_literal(attrs['partOfSpeech'], PARTS_OF_SPEECH),
+            script=attrs.get('script'),
+            pronunciations=_load_pronunciations(events),
+            tags=_load_tags(events)
+        )
+        events.end('Lemma')
+    return lemma
 
-def _load_form(local_root, events) -> Form:
-    attrs = local_root.attrib
-    return Form(
-        attrs['writtenForm'],
-        script=attrs.get('script'),
-        tags=_load_tags_until(events, 'Form'))
 
+def _load_forms(events, external) -> List[Form]:
+    forms: List[Form] = []
+    while True:
+        if events.starts('Form'):
+            attrs = next(events)[1].attrib
+            forms.append(
+                Form(attrs.get('id'),
+                     attrs['writtenForm'],
+                     script=attrs.get('script'),
+                     pronunciations=_load_pronunciations(events),
+                     tags=_load_tags(events))
+            )
+            events.end('Form')
+        elif external and events.starts('ExternalForm'):
+            attrs = next(events)[1].attrib
+            forms.append(
+                Form.as_external(
+                    attrs['id'],
+                    pronunciations=_load_pronunciations(events),
+                    tags=_load_tags(events)
+                )
+            )
+            events.end('ExternalForm')
+        else:
+            break
+    return forms
+
+
+def _load_pronunciations(events) -> List[Pronunciation]:
+    pronunciations: List[Pronunciation] = []
+    while events.starts('Pronunciation'):
+        next(events)
+        elem = events.end('Pronunciation')
+        attrs = elem.attrib
+        pronunciations.append(
+            Pronunciation(
+                elem.text,
+                variety=attrs.get('variety'),
+                notation=attrs.get('notation'),
+                phonemic=_get_bool(attrs.get('phonemic', 'true')),
+                audio=attrs.get('audio'),
+            )
+        )
+    return pronunciations
 
-def _load_tags_until(events, terminus) -> List[Tag]:
-    event, elem = next(events)
 
+def _load_tags(events) -> List[Tag]:
     tags: List[Tag] = []
-    while event == 'start' and elem.tag == 'Tag':
-        event, elem = next(events)
-        _assert_closed(event, elem, 'Tag')
+    while events.starts('Tag'):
+        next(events)
+        elem = events.end('Tag')
         tags.append(Tag(elem.text, elem.attrib['category']))
-        event, elem = next(events)
-
-    _assert_closed(event, elem, terminus)
-
     return tags
 
 
-def _load_sense(local_root, events, version) -> Sense:
-    attrs = local_root.attrib
-    event, elem = next(events)
-
+def _load_senses(events, sbmap, external, version) -> List[Sense]:
+    senses: List[Sense] = []
+    while True:
+        if events.starts('Sense'):
+            attrs = events.start('Sense').attrib
+            sense = Sense(
+                attrs['id'],
+                attrs['synset'],
+                relations=_load_sense_relations(events, version),
+                examples=_load_examples(events, version),
+                counts=_load_counts(events, version),
+                lexicalized=_get_bool(attrs.get('lexicalized', 'true')),
+                adjposition=_get_literal(attrs.get('adjposition'), ADJPOSITIONS),
+                meta=_get_metadata(attrs, version),
+            )
+            for sbid in attrs.get('subcat', '').split():
+                sbmap.setdefault(sbid, []).append(attrs['id'])
+            events.end('Sense')
+        elif external and events.starts('ExternalSense'):
+            attrs = events.start('ExternalSense').attrib
+            sense = Sense.as_external(
+                attrs['id'],
+                relations=_load_sense_relations(events, version),
+                examples=_load_examples(events, version),
+                counts=_load_counts(events, version),
+            )
+            events.end('ExternalSense')
+        else:
+            break
+        senses.append(sense)
+    return senses
+
+
+def _load_sense_relations(events, version) -> List[SenseRelation]:
     relations: List[SenseRelation] = []
-    while event == 'start' and elem.tag == 'SenseRelation':
-        event, elem = next(events)
-        _assert_closed(event, elem, 'SenseRelation')
+    while events.starts('SenseRelation'):
+        next(events)
+        elem = events.end('SenseRelation')
+        attrs = elem.attrib
+        reltype = attrs['relType']
+        if not (reltype in SENSE_RELATIONS or reltype in SENSE_SYNSET_RELATIONS):
+            raise LMFError(f'invalid sense relation: {reltype}')
         relations.append(
-            _load_relation(elem, SenseRelation, SENSE_RELATIONS, version)
+            SenseRelation(
+                attrs['target'],
+                reltype,
+                meta=_get_metadata(attrs, version)
+            )
         )
-        event, elem = next(events)
+    return relations
 
-    examples: List[Example] = []
-    while event == 'start' and elem.tag == 'Example':
-        event, elem = next(events)
-        _assert_closed(event, elem, 'Example')
-        examples.append(_load_example(elem, version))
-        event, elem = next(events)
-
-    counts: List[Count] = []
-    while event == 'start' and elem.tag == 'Count':
-        event, elem = next(events)
-        _assert_closed(event, elem, 'Count')
-        counts.append(_load_count(elem, version))
-        event, elem = next(events)
-
-    _assert_closed(event, elem, 'Sense')
-
-    return Sense(
-        attrs['id'],
-        attrs['synset'],
-        relations=relations,
-        examples=examples,
-        counts=counts,
-        lexicalized=_get_bool(attrs.get('lexicalized', 'true')),
-        adjposition=_get_optional_literal(attrs.get('adjposition'), ADJPOSITIONS),
-        meta=_get_metadata(attrs, version),
-    )
 
-
-_R = TypeVar('_R', SynsetRelation, SenseRelation)
+def _load_examples(events, version) -> List[Example]:
+    examples: List[Example] = []
+    while events.starts('Example'):
+        next(events)
+        elem = events.end('Example')
+        attrs = elem.attrib
+        examples.append(
+            Example(
+                elem.text,
+                language=attrs.get('language'),
+                meta=_get_metadata(attrs, version),
+            )
+        )
+    return examples
 
 
-def _load_relation(elem, cls: Type[_R], choices: Container[str], version: str) -> _R:
-    attrs = elem.attrib
-    return cls(
-        attrs['target'],
-        _get_literal
-        (attrs['relType'], choices),
-        meta=_get_metadata(attrs, version),
-    )
+def _load_counts(events, version) -> List[Count]:
+    counts: List[Count] = []
+    while events.starts('Count'):
+        next(events)
+        elem = events.end('Count')
+        attrs = elem.attrib
+        value: int = -1
+        try:
+            value = int(elem.text)
+        except ValueError:
+            warnings.warn(f'count must be an integer: {elem.text}', LMFWarning)
 
+        counts.append(Count(value, meta=_get_metadata(attrs, version)))
 
-def _load_example(elem, version) -> Example:
-    attrs = elem.attrib
-    return Example(
-        elem.text,
-        language=attrs.get('language'),
-        meta=_get_metadata(attrs, version),
-    )
+    return counts
 
 
-def _load_count(elem, version) -> Count:
-    attrs = elem.attrib
-    value: int = -1
-    try:
-        value = int(elem.text)
-    except ValueError:
-        warnings.warn(f'count must be an integer: {elem.text}', LMFWarning)
-    return Count(
-        value,
-        meta=_get_metadata(attrs, version),
-    )
+def _load_syntactic_behaviours(events, version) -> List[SyntacticBehaviour]:
+    syntactic_behaviours: List[SyntacticBehaviour] = []
+    while events.starts('SyntacticBehaviour'):
+        next(events)
+        attrs = events.end('SyntacticBehaviour').attrib
+        syntactic_behaviours.append(
+            SyntacticBehaviour(
+                attrs.get('id'),
+                attrs['subcategorizationFrame'],
+                senses=attrs.get('senses', '').split(),
+            )
+        )
+    return syntactic_behaviours
 
 
-def _load_syntactic_behaviour(elem) -> SyntacticBehaviour:
-    attrs = elem.attrib
-    return SyntacticBehaviour(
-        attrs['subcategorizationFrame'],
-        senses=attrs.get('senses', '').split(),
-    )
+def _load_synsets(
+    events: XMLEventIterator,
+    extension: bool,
+    version: str,
+    lex_root: ET.Element,
+) -> List[Synset]:
+    synsets: List[Synset] = []
+    while True:
+        if events.starts('Synset'):
+            attrs = events.start('Synset').attrib
+            synset = Synset(
+                attrs['id'],
+                attrs['ili'],
+                pos=_get_literal(attrs.get('partOfSpeech'), PARTS_OF_SPEECH),
+                definitions=_load_definitions(events, version),
+                ili_definition=_load_ilidefinition(events, version),
+                relations=_load_synset_relations(events, version),
+                examples=_load_examples(events, version),
+                lexicalized=_get_bool(attrs.get('lexicalized', 'true')),
+                members=attrs.get('members', '').split(),
+                lexfile=_get_literal(attrs.get('lexfile'), LEXICOGRAPHER_FILES),
+                meta=_get_metadata(attrs, version),
+            )
+            events.end('Synset')
+        elif extension and events.starts('ExternalSynset'):
+            attrs = events.start('ExternalSynset').attrib
+            synset = Synset.as_external(
+                attrs['id'],
+                definitions=_load_definitions(events, version),
+                relations=_load_synset_relations(events, version),
+                examples=_load_examples(events, version),
+            )
+            events.end('ExternalSynset')
+        else:
+            break
+        synsets.append(synset)
+        lex_root.clear()
+
+    return synsets
+
+
+def _load_definitions(events, version) -> List[Definition]:
+    definitions: List[Definition] = []
+    while events.starts('Definition'):
+        next(events)
+        elem = events.end('Definition')
+        attrs = elem.attrib
+        definitions.append(
+            Definition(
+                elem.text,
+                language=attrs.get('language'),
+                source_sense=attrs.get('sourceSense'),
+                meta=_get_metadata(attrs, version),
+            )
+        )
+    return definitions
 
 
-def _load_synset(local_root, events, version) -> Synset:
-    attrs = local_root.attrib
-    event, elem = next(events)
+def _load_ilidefinition(events, version) -> Optional[ILIDefinition]:
+    ili_definition: Optional[ILIDefinition] = None
+    if events.starts('ILIDefinition'):
+        next(events)
+        elem = events.end('ILIDefinition')
+        ili_definition = ILIDefinition(
+            elem.text,
+            meta=_get_metadata(elem.attrib, version),
+        )
+    return ili_definition
 
-    definitions: List[Definition] = []
-    while event == 'start' and elem.tag == 'Definition':
-        event, elem = next(events)
-        _assert_closed(event, elem, 'Definition')
-        definitions.append(_load_definition(elem, version))
-        event, elem = next(events)
-
-    ili_definition = None
-    if event == 'start' and elem.tag == 'ILIDefinition':
-        event, elem = next(events)
-        _assert_closed(event, elem, 'ILIDefinition')
-        ili_definition = _load_ilidefinition(elem, version)
-        event, elem = next(events)
 
+def _load_synset_relations(events, version: str) -> List[SynsetRelation]:
     relations: List[SynsetRelation] = []
-    while event == 'start' and elem.tag == 'SynsetRelation':
-        event, elem = next(events)
-        _assert_closed(event, elem, 'SynsetRelation')
+    while events.starts('SynsetRelation'):
+        next(events)
+        elem = events.end('SynsetRelation')
+        attrs = elem.attrib
+        reltype = attrs['relType']
+        if reltype not in SYNSET_RELATIONS:
+            raise LMFError(f'invalid synset relation: {reltype}')
         relations.append(
-            _load_relation(elem, SynsetRelation, SYNSET_RELATIONS, version)
+            SynsetRelation(
+                attrs['target'],
+                reltype,
+                meta=_get_metadata(attrs, version),
+            )
         )
-        event, elem = next(events)
-
-    examples: List[Example] = []
-    while event == 'start' and elem.tag == 'Example':
-        event, elem = next(events)
-        _assert_closed(event, elem, 'Example')
-        examples.append(_load_example(elem, version))
-        event, elem = next(events)
-
-    _assert_closed(event, elem, 'Synset')
-
-    return Synset(
-        attrs['id'],
-        attrs['ili'],
-        pos=_get_optional_literal(attrs.get('partOfSpeech'), PARTS_OF_SPEECH),
-        definitions=definitions,
-        ili_definition=ili_definition,
-        relations=relations,
-        examples=examples,
-        lexicalized=_get_bool(attrs.get('lexicalized', 'true')),
-        meta=_get_metadata(attrs, version),
-    )
-
-
-def _load_definition(elem, version) -> Definition:
-    attrs = elem.attrib
-    return Definition(
-        elem.text,
-        language=attrs.get('language'),
-        source_sense=attrs.get('sourceSense'),
-        meta=_get_metadata(attrs, version),
-    )
-
-
-def _load_ilidefinition(elem, version) -> ILIDefinition:
-    attrs = elem.attrib
-    return ILIDefinition(
-        elem.text,
-        meta=_get_metadata(attrs, version),
-    )
+    return relations
 
 
 def _get_bool(value: str) -> bool:
-    value = value.lower()
-    if value not in ('true', 'false'):
-        warnings.warn(f'value must be "true" or "false", not {value!r}', LMFWarning)
-    return value == 'true'
-
-
-def _get_optional_literal(value: Optional[str], choices: Container[str]) -> str:
-    if value is None:
-        return ''
-    else:
-        return _get_literal(value, choices)
+    return _get_literal(value.lower(), ('true', 'false')) == 'true'
 
 
-def _get_literal(value: str, choices: Container[str]) -> str:
+def _get_literal(value: Optional[str], choices: Container[str]) -> str:
     if value is not None and value not in choices:
         warnings.warn(f'{value!r} is not one of {choices!r}', LMFWarning)
-    return value
+    return value or ''
 
 
 def _get_metadata(attrs: Dict, version: str) -> Optional[Metadata]:
@@ -719,16 +966,30 @@ def _get_metadata(attrs: Dict, version: str) -> Optional[Metadata]:
         return None
 
 
-def _assert_closed(event, elem, tag) -> None:
-    if event != 'end' or elem.tag != tag:
-        raise _unexpected(elem)
-
+def dump(
+        lexicons: LexicalResource, destination: AnyPath, version: str = '1.0'
+) -> None:
+    """Write wordnets in the WN-LMF format.
 
-def _unexpected(elem) -> LMFError:
-    return LMFError(f'unexpected element: {elem.tag}')
+    Args:
+        lexicons: a list of :class:`Lexicon` objects
+    """
+    if version not in _SCHEMAS:
+        raise LMFError(f'invalid version: {version}')
+    destination = Path(destination).expanduser()
+    doctype = _DOCTYPE.format(schema=_SCHEMAS[version])
+    dc_uri = _DC_URIS[version]
+    with destination.open('wt', encoding='utf-8') as out:
+        print(_XMLDECL.decode('utf-8'), file=out)
+        print(doctype, file=out)
+        print(f'<LexicalResource xmlns:dc="{dc_uri}">', file=out)
+        for lexicon in lexicons:
+            _dump_lexicon(lexicon, out, version)
+        print('</LexicalResource>', file=out)
 
 
 def _dump_lexicon(lexicon: Lexicon, out: TextIO, version: str) -> None:
+    lexicontype = 'LexiconExtension' if lexicon.extends else 'Lexicon'
     attrib = {
         'id': lexicon.id,
         'label': lexicon.label,
@@ -741,76 +1002,160 @@ def _dump_lexicon(lexicon: Lexicon, out: TextIO, version: str) -> None:
         attrib['url'] = lexicon.url
     if lexicon.citation:
         attrib['citation'] = lexicon.citation
+    if version != '1.0' and lexicon.logo:
+        attrib['logo'] = lexicon.logo
     attrib.update(_meta_dict(lexicon.meta))
-    attrdelim = '\n' + (' ' * 11)
+    attrdelim = '\n' + (' ' * len(f'  <{lexicontype} '))
     attrs = attrdelim.join(
         f'{attr}={quoteattr(str(val))}' for attr, val in attrib.items()
     )
-    print(f'  <Lexicon {attrs}>', file=out)
+    print(f'  <{lexicontype} {attrs}>', file=out)
+
+    if version != '1.0':
+        if lexicontype == 'LexiconExtension':
+            assert lexicon.extends is not None
+            _dump_dependency(lexicon.extends, 'Extends', out)
+        for req in lexicon.requires:
+            _dump_dependency(req, 'Requires', out)
+    sbmap: Dict[str, List[SyntacticBehaviour]] = {}
+    for sb in lexicon.syntactic_behaviours:
+        for sense_id in sb.senses:
+            sbmap.setdefault(sense_id, []).append(sb)
 
     for entry in lexicon.lexical_entries:
-        # TODO: 1.0 SyntacticBehaviour
-        _dump_lexical_entry(entry, out, version)
+        _dump_lexical_entry(entry, out, sbmap, version)
 
     for synset in lexicon.synsets:
         _dump_synset(synset, out, version)
 
-    # TODO: 1.1 SyntacticBehaviour
+    if version != '1.0':
+        for sb in lexicon.syntactic_behaviours:
+            _dump_syntactic_behaviour(sb, out, version)
+
+    print(f'  </{lexicontype}>', file=out)
+
 
-    print('  </Lexicon>', file=out)
+def _dump_dependency(
+    dep: Dict[str, str], deptype: str, out: TextIO
+) -> None:
+    attrib = {'id': dep['id'], 'version': dep['version']}
+    if dep.get('url'):
+        attrib['url'] = dep['url']
+    elem = ET.Element(deptype, attrib=attrib)
+    print(_tostring(elem, 2), file=out)
 
 
 def _dump_lexical_entry(
-        entry: LexicalEntry, out: TextIO, version: str
+    entry: LexicalEntry,
+    out: TextIO,
+    sbmap: Dict[str, List[SyntacticBehaviour]],
+    version: str,
 ) -> None:
     attrib = {'id': entry.id}
-    attrib.update(_meta_dict(entry.meta))
-    elem = ET.Element('LexicalEntry', attrib=attrib)
-    elem.append(_build_lemma(entry.lemma))
-    elem.extend(_build_form(form) for form in entry.forms)
-    elem.extend(_build_sense(sense) for sense in entry.senses)
-    # TODO: 1.0 SyntacticBehaviour
+    if entry.external:
+        elem = ET.Element('ExternalLexicalEntry', attrib=attrib)
+    else:
+        attrib.update(_meta_dict(entry.meta))
+        elem = ET.Element('LexicalEntry', attrib=attrib)
+        assert entry.lemma is not None
+    if entry.lemma:
+        elem.append(_build_lemma(entry.lemma, version))
+    elem.extend(_build_form(form, version) for form in entry.forms)
+    elem.extend(_build_sense(sense, sbmap, version) for sense in entry.senses)
+    if version == '1.0':
+        senses = set(sense.id for sense in entry.senses)
+        for sense_id in senses:
+            for sb in sbmap.get(sense_id, []):
+                elem.append(
+                    _build_syntactic_behaviour_1_0(
+                        sb.frame, sorted(senses.intersection(sb.senses))
+                    )
+                )
     print(_tostring(elem, 2), file=out)
 
 
-def _build_lemma(lemma: Lemma) -> ET.Element:
-    attrib = {'writtenForm': lemma.form}
-    if lemma.script:
-        attrib['script'] = lemma.script
-    attrib['partOfSpeech'] = lemma.pos
-    elem = ET.Element('Lemma', attrib=attrib)
-    # TODO: Pronunciation
+def _build_syntactic_behaviour_1_0(frame: str, senses: List[str]) -> ET.Element:
+    return ET.Element(
+        'SyntacticBehaviour',
+        attrib={'subcategorizationFrame': frame, 'senses': ' '.join(senses)}
+    )
+
+
+def _build_lemma(lemma: Lemma, version: str) -> ET.Element:
+    if lemma.external:
+        elem = ET.Element('ExternalLemma')
+    else:
+        attrib = {'writtenForm': lemma.form}
+        if lemma.script:
+            attrib['script'] = lemma.script
+        attrib['partOfSpeech'] = lemma.pos
+        elem = ET.Element('Lemma', attrib=attrib)
+    if version != '1.0':
+        for pron in lemma.pronunciations:
+            elem.append(_build_pronunciation(pron))
     for tag in lemma.tags:
         elem.append(_build_tag(tag))
     return elem
 
 
-def _build_form(form: Form) -> ET.Element:
-    attrib = {'writtenForm': form.form}
-    if form.script:
-        attrib['script'] = form.script
-    elem = ET.Element('Form', attrib=attrib)
-    # TODO: Pronunciation
+def _build_form(form: Form, version: str) -> ET.Element:
+    attrib = {}
+    if version != '1.0' and form.id:
+        attrib['id'] = form.id
+    if form.external:
+        elem = ET.Element('ExternalForm', attrib=attrib)
+    else:
+        attrib['writtenForm'] = form.form
+        if form.script:
+            attrib['script'] = form.script
+        elem = ET.Element('Form', attrib=attrib)
+    if version != '1.0':
+        for pron in form.pronunciations:
+            elem.append(_build_pronunciation(pron))
     for tag in form.tags:
         elem.append(_build_tag(tag))
     return elem
 
 
+def _build_pronunciation(pron) -> ET.Element:
+    attrib = {}
+    if pron.variety:
+        attrib['variety'] = pron.variety
+    if pron.notation:
+        attrib['notation'] = pron.notation
+    if not pron.phonemic:
+        attrib['phonemic'] = 'false'
+    if pron.audio:
+        attrib['audio'] = pron.audio
+    elem = ET.Element('Pronunciation', attrib=attrib)
+    elem.text = pron.value
+    return elem
+
+
 def _build_tag(tag: Tag) -> ET.Element:
     elem = ET.Element('Tag', category=tag.category)
     elem.text = tag.text
     return elem
 
 
-def _build_sense(sense: Sense) -> ET.Element:
-    attrib = {'id': sense.id, 'synset': sense.synset}
-    attrib.update(_meta_dict(sense.meta))
-    if not sense.lexicalized:
-        attrib['lexicalized'] = 'false'
-    if sense.adjposition:
-        attrib['adjposition'] = sense.adjposition
-    # TODO: subcat
-    elem = ET.Element('Sense', attrib=attrib)
+def _build_sense(
+    sense: Sense,
+    sbmap: Dict[str, List[SyntacticBehaviour]],
+    version: str,
+) -> ET.Element:
+    attrib = {'id': sense.id}
+    if sense.external:
+        elem = ET.Element('ExternalSense', attrib=attrib)
+    else:
+        attrib['synset'] = sense.synset
+        attrib.update(_meta_dict(sense.meta))
+        if not sense.lexicalized:
+            attrib['lexicalized'] = 'false'
+        if sense.adjposition:
+            attrib['adjposition'] = sense.adjposition
+        if version != '1.0' and sense.id in sbmap:
+            attrib['subcat'] = ' '.join(sb.id for sb in sbmap[sense.id] if sb.id)
+        elem = ET.Element('Sense', attrib=attrib)
     elem.extend(_build_sense_relation(rel) for rel in sense.relations)
     elem.extend(_build_example(ex) for ex in sense.examples)
     elem.extend(_build_count(cnt) for cnt in sense.counts)
@@ -838,15 +1183,24 @@ def _build_count(count: Count) -> ET.Element:
 
 
 def _dump_synset(synset: Synset, out: TextIO, version: str) -> None:
-    attrib = {'id': synset.id, 'ili': synset.ili}
-    if synset.pos:
-        attrib['partOfSpeech'] = synset.pos
-    attrib.update(_meta_dict(synset.meta))
-    if not synset.lexicalized:
-        attrib['lexicalized'] = 'false'
-    elem = ET.Element('Synset', attrib=attrib)
+    attrib: Dict[str, str] = {'id': synset.id}
+    if synset.external:
+        elem = ET.Element('ExternalSynset', attrib=attrib)
+    else:
+        attrib['ili'] = synset.ili or ''
+        if synset.pos:
+            attrib['partOfSpeech'] = synset.pos
+        if not synset.lexicalized:
+            attrib['lexicalized'] = 'false'
+        if version != '1.0':
+            if synset.members:
+                attrib['members'] = ' '.join(synset.members)
+            if synset.lexfile:
+                attrib['lexfile'] = synset.lexfile
+        attrib.update(_meta_dict(synset.meta))
+        elem = ET.Element('Synset', attrib=attrib)
     elem.extend(_build_definition(defn) for defn in synset.definitions)
-    if synset.ili_definition:
+    if synset.ili_definition and not synset.external:
         elem.append(_build_ili_definition(synset.ili_definition))
     elem.extend(_build_synset_relation(rel) for rel in synset.relations)
     elem.extend(_build_example(ex) for ex in synset.examples)
@@ -880,16 +1234,18 @@ def _build_synset_relation(relation: SynsetRelation) -> ET.Element:
 def _dump_syntactic_behaviour(
         syntactic_behaviour: SyntacticBehaviour, out: TextIO, version: str
 ) -> None:
-    elem = _build_syntactic_behaviour(syntactic_behaviour)
+    elem = _build_syntactic_behaviour(syntactic_behaviour, version=version)
     print('    ' + _tostring(elem, 2), file=out)
 
 
-def _build_syntactic_behaviour(syntactic_behaviour: SyntacticBehaviour) -> ET.Element:
+def _build_syntactic_behaviour(
+    syntactic_behaviour: SyntacticBehaviour, version: str
+) -> ET.Element:
     elem = ET.Element('SyntacticBehaviour')
-    # if getattr(syntactic_behaviour, 'id', None):
-    #     elem.set('id', syntactic_behaviour.id)
+    if version != '1.0' and syntactic_behaviour.id:
+        elem.set('id', syntactic_behaviour.id)
     elem.set('subcategorizationFrame', syntactic_behaviour.frame)
-    if syntactic_behaviour.senses:
+    if version == '1.0' and syntactic_behaviour.senses:
         elem.set('senses', ' '.join(syntactic_behaviour.senses))
     return elem
 
@@ -918,7 +1274,7 @@ def _indent(elem: ET.Element, level: int) -> None:
         elem[-1].tail = self_indent
 
 
-def _meta_dict(m: Optional[Metadata]) -> Dict:
+def _meta_dict(m: Optional[Metadata]) -> Dict[str, str]:
     if m:
         d = {f'dc:{key}': str(val)
              for key, val in zip(m._fields, m)
diff --git a/wn/project.py b/wn/project.py
index ba0db27..638d2bd 100644
--- a/wn/project.py
+++ b/wn/project.py
@@ -1,6 +1,6 @@
 
 """
-Wordnet Packages and Collections
+Wordnet and ILI Packages and Collections
 """
 
 from typing import Optional, Iterator, List, Tuple
@@ -15,29 +15,48 @@
 
 import wn
 from wn._types import AnyPath
+from wn.constants import _WORDNET, _ILI
 from wn._util import is_gzip, is_lzma
 from wn import lmf
+from wn import _ili
 
 
 _ADDITIONAL_FILE_SUFFIXES = ('', '.txt', '.md', '.rst')
 
 
 def is_package_directory(path: AnyPath) -> bool:
-    """Return ``True`` if *path* appears to be a Wordnet Package."""
+    """Return ``True`` if *path* appears to be a wordnet or ILI package."""
     path = Path(path).expanduser()
-    return (path.is_dir()
-            and len(list(filter(lmf.is_lmf, path.iterdir()))) == 1)
+    return len(_package_directory_types(path)) == 1
+
+
+def _package_directory_types(path: Path) -> List[Tuple[Path, str]]:
+    types: List[Tuple[Path, str]] = []
+    if path.is_dir():
+        for p in path.iterdir():
+            typ = _resource_file_type(p)
+            if typ is not None:
+                types.append((p, typ))
+    return types
+
+
+def _resource_file_type(path: Path) -> Optional[str]:
+    if lmf.is_lmf(path):
+        return _WORDNET
+    elif _ili.is_ili(path):
+        return _ILI
+    return None
 
 
 def is_collection_directory(path: AnyPath) -> bool:
-    """Return ``True`` if *path* appears to be a Wordnet Collection."""
+    """Return ``True`` if *path* appears to be a wordnet collection."""
     path = Path(path).expanduser()
     return (path.is_dir()
             and len(list(filter(is_package_directory, path.iterdir()))) >= 1)
 
 
 class _Project:
-    __slots__ = '_path'
+    __slots__ = '_path',
 
     def __init__(self, path: AnyPath):
         self._path: Path = Path(path).expanduser()
@@ -63,19 +82,23 @@ def _find_file(self, base: Path, suffixes: Tuple[str, ...]) -> Optional[Path]:
 
 
 class Package(_Project):
-    """This class represents a Wordnet Package -- a directory with a
-       resource file and optional metadata.
+    """This class represents a wordnet or ILI package -- a directory with
+       a resource file and optional metadata.
 
     """
 
+    @property
+    def type(self) -> Optional[str]:
+        return _resource_file_type(self.resource_file())
+
     def resource_file(self) -> Path:
         """Return the path of the package's resource file."""
-        files = list(filter(lmf.is_lmf, self._path.iterdir()))
+        files = _package_directory_types(self._path)
         if not files:
             raise wn.Error(f'no resource found in package: {self._path!s}')
         elif len(files) > 1:
             raise wn.Error(f'multiple resource found in package: {self._path!s}')
-        return files[0]
+        return files[0][0]
 
 
 class _ResourceOnlyPackage(Package):
@@ -89,8 +112,8 @@ def citation(self): return None
 
 
 class Collection(_Project):
-    """This class represents a Wordnet Collection -- a directory with one
-       or more Wordnet Packages and optional metadata.
+    """This class represents a wordnet or ILI collection -- a directory
+       with one or more wordnet/ILI packages and optional metadata.
 
     """
 
@@ -102,10 +125,10 @@ def packages(self) -> List[Package]:
 
 
 def iterpackages(path: AnyPath) -> Iterator[Package]:
-    """Yield any wordnet Packages found at *path*.
+    """Yield any wordnet or ILI packages found at *path*.
 
     The *path* argument can point to one of the following:
-      - a lexical resource file
+      - a lexical resource file or ILI file
       - a wordnet package directory
       - a wordnet collection directory
       - a tar archive containing one of the above
@@ -140,7 +163,7 @@ def iterpackages(path: AnyPath) -> Iterator[Package]:
     else:
         decompressed: Path
         with _get_decompressed(path) as decompressed:
-            if lmf.is_lmf(decompressed):
+            if lmf.is_lmf(decompressed) or _ili.is_ili(decompressed):
                 yield _ResourceOnlyPackage(decompressed)
             else:
                 raise wn.Error(
@@ -164,10 +187,14 @@ def _get_decompressed(source: Path) -> Iterator[Path]:
             else:  # xzipped
                 with lzma.open(source, 'rb') as lzma_src:
                     shutil.copyfileobj(lzma_src, tmp)
+
             tmp.close()  # Windows cannot reliably reopen until it's closed
+
             yield path
+
         except (OSError, EOFError, lzma.LZMAError) as exc:
             raise wn.Error(f'could not decompress file: {source}') from exc
+
         finally:
             path.unlink()
 
diff --git a/wn/schema.sql b/wn/schema.sql
index a5b532a..44156c2 100644
--- a/wn/schema.sql
+++ b/wn/schema.sql
@@ -1,4 +1,28 @@
 
+-- ILI : Interlingual Index
+
+CREATE TABLE ilis (
+    rowid INTEGER PRIMARY KEY,
+    id TEXT NOT NULL,
+    status_rowid INTEGER NOT NULL REFERENCES ili_statuses (rowid),
+    definition TEXT,
+    metadata META,
+    UNIQUE (id)
+);
+CREATE INDEX ili_id_index ON ilis (id);
+
+CREATE TABLE proposed_ilis (
+    rowid INTEGER PRIMARY KEY,
+    synset_rowid INTEGER REFERENCES synsets (rowid) ON DELETE CASCADE,
+    definition TEXT,
+    metadata META,
+    UNIQUE (synset_rowid)
+);
+CREATE INDEX proposed_ili_synset_rowid_index ON proposed_ilis (synset_rowid);
+
+
+-- Wordnet lexicons
+
 CREATE TABLE lexicons (
     rowid INTEGER PRIMARY KEY,  -- unique database-internal id
     id TEXT NOT NULL,           -- user-facing id
@@ -9,24 +33,31 @@ CREATE TABLE lexicons (
     version TEXT NOT NULL,
     url TEXT,
     citation TEXT,
+    logo TEXT,
     metadata META,
+    modified BOOLEAN CHECK( modified IN (0, 1) ) DEFAULT 0 NOT NULL,
     UNIQUE (id, version)
 );
 
--- ILI : Interlingual Index
-
-CREATE TABLE ilis (
-    ili TEXT PRIMARY KEY,
-    definition TEXT,
-    metadata META
+CREATE TABLE lexicon_dependencies (
+    dependent_rowid INTEGER NOT NULL REFERENCES lexicons (rowid) ON DELETE CASCADE,
+    provider_id TEXT NOT NULL,
+    provider_version TEXT NOT NULL,
+    provider_url TEXT,
+    provider_rowid INTEGER REFERENCES lexicons (rowid) ON DELETE SET NULL
 );
+CREATE INDEX lexicon_dependent_index ON lexicon_dependencies(dependent_rowid);
 
-CREATE TABLE proposed_ilis (
-    synset_rowid INTEGER NOT NULL REFERENCES synsets(rowid) ON DELETE CASCADE,
-    definition TEXT,
-    metadata META
+CREATE TABLE lexicon_extensions (
+    extension_rowid INTEGER NOT NULL REFERENCES lexicons (rowid) ON DELETE CASCADE,
+    base_id TEXT NOT NULL,
+    base_version TEXT NOT NULL,
+    base_url TEXT,
+    base_rowid INTEGER REFERENCES lexicons (rowid),
+    UNIQUE (extension_rowid, base_rowid)
 );
-CREATE INDEX proposed_ili_synset_index ON proposed_ilis(synset_rowid);
+CREATE INDEX lexicon_extension_index ON lexicon_extensions(extension_rowid);
+
 
 -- Lexical Entries
 
@@ -38,8 +69,8 @@ CREATE INDEX proposed_ili_synset_index ON proposed_ilis(synset_rowid);
 CREATE TABLE entries (
     rowid INTEGER PRIMARY KEY,
     id TEXT NOT NULL,
-    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons (rowid),
-    pos_rowid INTEGER NOT NULL REFERENCES parts_of_speech (rowid),
+    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons (rowid) ON DELETE CASCADE,
+    pos TEXT NOT NULL,
     metadata META,
     UNIQUE (id, lexicon_rowid)
 );
@@ -47,71 +78,78 @@ CREATE INDEX entry_id_index ON entries (id);
 
 CREATE TABLE forms (
     rowid INTEGER PRIMARY KEY,
+    id TEXT,
+    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons(rowid) ON DELETE CASCADE,
     entry_rowid INTEGER NOT NULL REFERENCES entries(rowid) ON DELETE CASCADE,
     form TEXT NOT NULL,
+    normalized_form TEXT,
     script TEXT,
     rank INTEGER DEFAULT 1,  -- rank 0 is the preferred lemma
     UNIQUE (entry_rowid, form, script)
 );
 CREATE INDEX form_entry_index ON forms (entry_rowid);
 CREATE INDEX form_index ON forms (form);
+CREATE INDEX form_norm_index ON forms (normalized_form);
 
+CREATE TABLE pronunciations (
+    form_rowid INTEGER NOT NULL REFERENCES forms (rowid) ON DELETE CASCADE,
+    value TEXT,
+    variety TEXT,
+    notation TEXT,
+    phonemic BOOLEAN CHECK( phonemic IN (0, 1) ) DEFAULT 1 NOT NULL,
+    audio TEXT
+);
+CREATE INDEX pronunciation_form_index ON pronunciations (form_rowid);
 
 CREATE TABLE tags (
     form_rowid INTEGER NOT NULL REFERENCES forms (rowid) ON DELETE CASCADE,
     tag TEXT,
     category TEXT
 );
+CREATE INDEX tag_form_index ON tags (form_rowid);
 
-CREATE TABLE syntactic_behaviours (
-    id TEXT NOT NULL,
-    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons (rowid),
-    frame TEXT,
-    PRIMARY KEY (id, lexicon_rowid)
-);
-
-CREATE TABLE syntactic_behaviour_senses (
-    syntactic_behaviour_id TEXT NOT NULL,
-    lexicon_rowid INTEGER NOT NULL,
-    sense_rowid INTEGER NOT NULL REFERENCES senses (rowid) ON DELETE CASCADE,
-    FOREIGN KEY (syntactic_behaviour_id, lexicon_rowid) REFERENCES syntactic_behaviours (id, lexicon_rowid)
-);
-CREATE INDEX syntactic_behavior_sense_index ON syntactic_behaviour_senses (sense_rowid);
 
 -- Synsets
 
 CREATE TABLE synsets (
     rowid INTEGER PRIMARY KEY,
     id TEXT NOT NULL,
-    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons (rowid),
-    ili TEXT,
-    pos_rowid INTEGER NOT NULL REFERENCES parts_of_speech (rowid),
-    -- lexfile_id INTEGER REFERENCES lexicographer_files (id),
+    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons (rowid) ON DELETE CASCADE,
+    ili_rowid INTEGER REFERENCES ilis (rowid),
+    pos TEXT,
     lexicalized BOOLEAN CHECK( lexicalized IN (0, 1) ) DEFAULT 1 NOT NULL,
+    lexfile_rowid INTEGER REFERENCES lexfiles (rowid),
     metadata META
 );
 CREATE INDEX synset_id_index ON synsets (id);
-CREATE INDEX synset_ili_index ON synsets (ili);
+CREATE INDEX synset_ili_rowid_index ON synsets (ili_rowid);
 
 CREATE TABLE synset_relations (
+    rowid INTEGER PRIMARY KEY,
+    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons (rowid) ON DELETE CASCADE,
     source_rowid INTEGER NOT NULL REFERENCES synsets(rowid) ON DELETE CASCADE,
     target_rowid INTEGER NOT NULL REFERENCES synsets(rowid) ON DELETE CASCADE,
-    type_rowid INTEGER NOT NULL REFERENCES synset_relation_types (rowid),
+    type_rowid INTEGER NOT NULL REFERENCES relation_types(rowid),
     metadata META
 );
 CREATE INDEX synset_relation_source_index ON synset_relations (source_rowid);
 CREATE INDEX synset_relation_target_index ON synset_relations (target_rowid);
 
 CREATE TABLE definitions (
+    rowid INTEGER PRIMARY KEY,
+    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons(rowid) ON DELETE CASCADE,
     synset_rowid INTEGER NOT NULL REFERENCES synsets(rowid) ON DELETE CASCADE,
     definition TEXT,
     language TEXT,  -- bcp-47 language tag
-    -- sense_rowid INTEGER NOT NULL REFERENCES senses(rowid),
+    sense_rowid INTEGER REFERENCES senses(rowid) ON DELETE SET NULL,
     metadata META
 );
 CREATE INDEX definition_rowid_index ON definitions (synset_rowid);
+CREATE INDEX definition_sense_index ON definitions (sense_rowid);
 
 CREATE TABLE synset_examples (
+    rowid INTEGER PRIMARY KEY,
+    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons(rowid) ON DELETE CASCADE,
     synset_rowid INTEGER NOT NULL REFERENCES synsets(rowid) ON DELETE CASCADE,
     example TEXT,
     language TEXT,  -- bcp-47 language tag
@@ -119,46 +157,55 @@ CREATE TABLE synset_examples (
 );
 CREATE INDEX synset_example_rowid_index ON synset_examples(synset_rowid);
 
+
 -- Senses
 
 CREATE TABLE senses (
     rowid INTEGER PRIMARY KEY,
     id TEXT NOT NULL,
-    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons(rowid),
+    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons(rowid) ON DELETE CASCADE,
     entry_rowid INTEGER NOT NULL REFERENCES entries(rowid) ON DELETE CASCADE,
     entry_rank INTEGER DEFAULT 1,
     synset_rowid INTEGER NOT NULL REFERENCES synsets(rowid) ON DELETE CASCADE,
-    -- sense_key TEXT,  -- not actually UNIQUE ?
-    -- adjposition_rowid INTEGER REFERENCES adjpositions (rowid),
+    synset_rank INTEGER DEFAULT 1,
     lexicalized BOOLEAN CHECK( lexicalized IN (0, 1) ) DEFAULT 1 NOT NULL,
     metadata META
-    -- FOREIGN KEY (synset_id, lexicon_rowid) REFERENCES synsets (id, lexicon_rowid),
-    -- UNIQUE (id, lexicon_rowid)
 );
 CREATE INDEX sense_id_index ON senses(id);
 CREATE INDEX sense_entry_rowid_index ON senses (entry_rowid);
 CREATE INDEX sense_synset_rowid_index ON senses (synset_rowid);
 
 CREATE TABLE sense_relations (
+    rowid INTEGER PRIMARY KEY,
+    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons (rowid) ON DELETE CASCADE,
     source_rowid INTEGER NOT NULL REFERENCES senses(rowid) ON DELETE CASCADE,
     target_rowid INTEGER NOT NULL REFERENCES senses(rowid) ON DELETE CASCADE,
-    type_rowid TEXT NOT NULL REFERENCES sense_relation_types (rowid),
+    type_rowid INTEGER NOT NULL REFERENCES relation_types(rowid),
     metadata META
 );
 CREATE INDEX sense_relation_source_index ON sense_relations (source_rowid);
 CREATE INDEX sense_relation_target_index ON sense_relations (target_rowid);
 
 CREATE TABLE sense_synset_relations (
+    rowid INTEGER PRIMARY KEY,
+    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons (rowid) ON DELETE CASCADE,
     source_rowid INTEGER NOT NULL REFERENCES senses(rowid) ON DELETE CASCADE,
     target_rowid INTEGER NOT NULL REFERENCES synsets(rowid) ON DELETE CASCADE,
-    -- limit the type to ('domain_topic', 'domain_region', 'exemplifies') ?
-    type_rowid TEXT NOT NULL REFERENCES sense_relation_types (rowid),
+    type_rowid INTEGER NOT NULL REFERENCES relation_types(rowid),
     metadata META
 );
 CREATE INDEX sense_synset_relation_source_index ON sense_synset_relations (source_rowid);
 CREATE INDEX sense_synset_relation_target_index ON sense_synset_relations (target_rowid);
 
+CREATE TABLE adjpositions (
+    sense_rowid INTEGER NOT NULL REFERENCES senses(rowid) ON DELETE CASCADE,
+    adjposition TEXT NOT NULL
+);
+CREATE INDEX adjposition_sense_index ON adjpositions (sense_rowid);
+
 CREATE TABLE sense_examples (
+    rowid INTEGER PRIMARY KEY,
+    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons(rowid) ON DELETE CASCADE,
     sense_rowid INTEGER NOT NULL REFERENCES senses(rowid) ON DELETE CASCADE,
     example TEXT,
     language TEXT,  -- bcp-47 language tag
@@ -167,40 +214,56 @@ CREATE TABLE sense_examples (
 CREATE INDEX sense_example_index ON sense_examples (sense_rowid);
 
 CREATE TABLE counts (
+    rowid INTEGER PRIMARY KEY,
+    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons(rowid) ON DELETE CASCADE,
     sense_rowid INTEGER NOT NULL REFERENCES senses(rowid) ON DELETE CASCADE,
     count INTEGER NOT NULL,
     metadata META
 );
 CREATE INDEX count_index ON counts(sense_rowid);
 
--- Lookup tables
 
-CREATE TABLE parts_of_speech (
+-- Syntactic Behaviours
+
+CREATE TABLE syntactic_behaviours (
     rowid INTEGER PRIMARY KEY,
-    pos TEXT NOT NULL UNIQUE
+    id TEXT,
+    lexicon_rowid INTEGER NOT NULL REFERENCES lexicons (rowid) ON DELETE CASCADE,
+    frame TEXT NOT NULL,
+    UNIQUE (lexicon_rowid, id),
+    UNIQUE (lexicon_rowid, frame)
 );
-CREATE UNIQUE INDEX pos_index ON parts_of_speech (pos);
+CREATE INDEX syntactic_behaviour_id_index ON syntactic_behaviours (id);
 
-CREATE TABLE adjpositions (
-    rowid INTEGER PRIMARY KEY,
-    position TEXT NOT NULL UNIQUE
+CREATE TABLE syntactic_behaviour_senses (
+    syntactic_behaviour_rowid INTEGER NOT NULL REFERENCES syntactic_behaviours (rowid) ON DELETE CASCADE,
+    sense_rowid INTEGER NOT NULL REFERENCES senses (rowid) ON DELETE CASCADE
 );
-CREATE UNIQUE INDEX adposition_index ON adjpositions (position);
+CREATE INDEX syntactic_behaviour_sense_sb_index
+    ON syntactic_behaviour_senses (syntactic_behaviour_rowid);
+CREATE INDEX syntactic_behaviour_sense_sense_index
+    ON syntactic_behaviour_senses (sense_rowid);
 
-CREATE TABLE synset_relation_types (
+
+-- Lookup Tables
+
+CREATE TABLE relation_types (
     rowid INTEGER PRIMARY KEY,
-    type TEXT NOT NULL UNIQUE
+    type TEXT NOT NULL,
+    UNIQUE (type)
 );
-CREATE UNIQUE INDEX synset_relation_type_index ON synset_relation_types (type);
+CREATE INDEX relation_type_index ON relation_types (type);
 
-CREATE TABLE sense_relation_types (
+CREATE TABLE ili_statuses (
     rowid INTEGER PRIMARY KEY,
-    type TEXT NOT NULL UNIQUE
+    status TEXT NOT NULL,
+    UNIQUE (status)
 );
-CREATE UNIQUE INDEX sense_relation_type_index ON sense_relation_types (type);
+CREATE INDEX ili_status_index ON ili_statuses (status);
 
-CREATE TABLE lexicographer_files (
-    id INTEGER PRIMARY KEY,  -- this is actually an ID, not just a rowid
-    name TEXT NOT NULL UNIQUE
+CREATE TABLE lexfiles (
+    rowid INTEGER PRIMARY KEY,
+    name TEXT NOT NULL,
+    UNIQUE (name)
 );
-CREATE UNIQUE INDEX lexicographer_file_index ON lexicographer_files (name);
+CREATE INDEX lexfile_index ON lexfiles (name);
diff --git a/wn/util.py b/wn/util.py
index c6f346f..55a4959 100644
--- a/wn/util.py
+++ b/wn/util.py
@@ -88,26 +88,45 @@ class ProgressBar(ProgressHandler):
     """A :class:`ProgressHandler` subclass for printing a progress bar.
 
     Example:
-        >>> p = ProgressBar(message='Progress: ', total=10, unit='units')
+        >>> p = ProgressBar(message='Progress: ', total=10, unit=' units')
         >>> p.update(3)
         Progress: [#########                     ] (3/10 units)
 
+    See :meth:`format` for a description of how the progress bar is
+    formatted.
+
     """
 
-    FMT = '\r{message} [{fill:<{width}}] ({count}/{total} {unit}) {status}'
+    #: The default formatting template.
+    FMT = '\r{message}{bar}{counter}{status}'
 
     def update(self, n: int = 1) -> None:
+        """Increment the count by *n* and print the reformatted bar."""
         self.kwargs['count'] += n  # type: ignore
         s = self.format()
         if self.file:
             print('\r\033[K', end='', file=self.file)
             print(s, end='', file=self.file)
 
-    def set(self, **kwargs) -> None:
-        self.kwargs.update(**kwargs)
-        self.update(0)
-
     def format(self) -> str:
+        """Format and return the progress bar.
+
+        The bar is is formatted according to :attr:`FMT`, using
+        variables from :attr:`kwargs` and two computed variables:
+
+        - ``bar``: visualization of the progress bar, empty when
+          ``total`` is 0
+
+        - ``counter``: display of ``count``, ``total``, and ``units``
+
+        >>> p = ProgressBar(message='Progress', count=2, total=10, unit='K')
+        >>> p.format()
+        '\\rProgress [######                        ] (2/10K) '
+        >>> p = ProgressBar(count=2, status='Counting...')
+        >>> p.format()
+        '\\r (2) Counting...'
+
+        """
         _kw = self.kwargs
         width = 30
         total: int = _kw['total']  # type: ignore
@@ -119,13 +138,18 @@ def format(self) -> str:
             part = ((num % total) * 3) // total
             if part:
                 fill += '-='[part-1]
+            bar = f' [{fill:<{width}}]'
+            counter = f' ({count}/{total}{_kw["unit"]}) '
         else:
-            fill = '-' * width
+            bar = ''
+            counter = f' ({count}{_kw["unit"]}) '
 
-        return self.FMT.format(fill=fill, width=width, **_kw)
+        return self.FMT.format(bar=bar, counter=counter, **_kw)
 
     def flash(self, message: str) -> None:
+        """Overwrite the progress bar with *message*."""
         print(f'\r\033[K{message}', end='', file=self.file)
 
     def close(self) -> None:
+        """Print a newline so the last printed bar remains on screen."""
         print(file=self.file)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 394973f..819fff0 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -17,6 +17,11 @@ def mini_lmf_1_0(datadir):
     return datadir / 'mini-lmf-1.0.xml'
 
 
+@pytest.fixture(scope='session')
+def mini_lmf_1_1(datadir):
+    return datadir / 'mini-lmf-1.1.xml'
+
+
 @pytest.fixture(scope='session')
 def empty_db_dir():
     with tempfile.TemporaryDirectory('wn_data_empty') as dir:
@@ -25,10 +30,24 @@ def empty_db_dir():
 
 @pytest.fixture(scope='session')
 def mini_db_dir(mini_lmf_1_0):
-    with tempfile.TemporaryDirectory('wn_data_empty') as dir:
+    with tempfile.TemporaryDirectory('wn_data_mini') as dir:
+        old_data_dir = wn.config.data_directory
+        wn.config.data_directory = dir
+        wn.add(mini_lmf_1_0)
+        wn.config.data_directory = old_data_dir
+        yield Path(dir)
+        # close any open DB connections before teardown
+        for conn in wn._db.pool.values():
+            conn.close()
+
+
+@pytest.fixture(scope='session')
+def mini_db_1_1_dir(mini_lmf_1_0, mini_lmf_1_1):
+    with tempfile.TemporaryDirectory('wn_data_mini_1_1') as dir:
         old_data_dir = wn.config.data_directory
         wn.config.data_directory = dir
         wn.add(mini_lmf_1_0)
+        wn.add(mini_lmf_1_1)
         wn.config.data_directory = old_data_dir
         yield Path(dir)
         # close any open DB connections before teardown
@@ -48,3 +67,10 @@ def mini_db(monkeypatch, mini_db_dir):
     with monkeypatch.context() as m:
         m.setattr(wn.config, 'data_directory', mini_db_dir)
         yield
+
+
+@pytest.fixture
+def mini_db_1_1(monkeypatch, mini_db_1_1_dir):
+    with monkeypatch.context() as m:
+        m.setattr(wn.config, 'data_directory', mini_db_1_1_dir)
+        yield
diff --git a/tests/data/mini-lmf-1.0.xml b/tests/data/mini-lmf-1.0.xml
index f0b28b2..e54564a 100644
--- a/tests/data/mini-lmf-1.0.xml
+++ b/tests/data/mini-lmf-1.0.xml
@@ -8,6 +8,7 @@ English:
 - information ⊃ (example, illustration) ⊃ sample ⊃ random sample
 - example ⊳ exemplify
 - illustration ⊳ illustrate
+- resignate
 
 Spanish:
 - información, ejemplo, ilustración, muestra aleatoria
@@ -26,8 +27,12 @@ Spanish:
            url="https://example.com/test-en">
 
     <LexicalEntry id="test-en-information-n">
-      <Lemma partOfSpeech="n" writtenForm="information" />
-      <Sense id="test-en-information-n-0001-01" synset="test-en-0001-n" />
+      <Lemma partOfSpeech="n" writtenForm="information" script="Latn">
+        <Tag category="tag-category">tag-text</Tag>
+      </Lemma>
+      <Sense id="test-en-information-n-0001-01" synset="test-en-0001-n">
+        <Count dc:source="some corpus">3</Count>
+      </Sense>
     </LexicalEntry>
 
     <LexicalEntry id="test-en-example-n">
@@ -45,6 +50,7 @@ Spanish:
     <LexicalEntry id="test-en-random_sample-n">
       <Lemma partOfSpeech="n" writtenForm="random sample" />
       <Sense id="test-en-random_sample-n-0005-01" synset="test-en-0005-n" />
+      <Sense id="test-en-random_sample-n-0005-02" synset="test-en-0005-n-fake" lexicalized="false" />
     </LexicalEntry>
 
     <LexicalEntry id="test-en-exemplify-v">
@@ -53,7 +59,7 @@ Spanish:
       <Form writtenForm="exemplified" />
       <Form writtenForm="exemplifying" />
       <Sense id="test-en-exemplify-v-0003-01" synset="test-en-0003-v" >
-        <SenseRelation relType="derivation" target="test-en-example-v-0002-01" />
+        <SenseRelation relType="derivation" target="test-en-example-n-0002-01" />
       </Sense>
     </LexicalEntry>
 
@@ -67,8 +73,10 @@ Spanish:
     <LexicalEntry id="test-en-illustrate-v">
       <Lemma partOfSpeech="v" writtenForm="illustrate" />
       <Sense id="test-en-illustrate-v-0004-01" synset="test-en-0003-v" >
-        <SenseRelation relType="derivation" target="test-en-illustration-v-0002-01" />
+        <SenseRelation relType="derivation" target="test-en-illustration-n-0002-01" />
       </Sense>
+      <SyntacticBehaviour senses="test-en-illustrate-v-0004-01" subcategorizationFrame="Somebody ----s something" />
+      <SyntacticBehaviour senses="test-en-illustrate-v-0004-01" subcategorizationFrame="Something ----s something" />
     </LexicalEntry>
 
     <LexicalEntry id="test-en-datum-n">
@@ -76,8 +84,13 @@ Spanish:
       <Sense id="test-en-datum-n-0006-01" synset="test-en-0006-n" />
     </LexicalEntry>
 
+    <LexicalEntry id="test-en-resignate-v">
+      <Lemma partOfSpeech="v" writtenForm="resignate" />
+      <Sense id="test-en-resignate-v-0007-01" synset="test-en-0007-v" />
+    </LexicalEntry>
+
     <Synset id="test-en-0001-n" ili="i67447" partOfSpeech="n" dc:subject="noun.cognition">
-      <Definition>something that informs</Definition>
+      <Definition sourceSense="test-en-information-n-0001-01">something that informs</Definition>
       <SynsetRelation relType="hyponym" target="test-en-0002-n" />
       <SynsetRelation relType="hyponym" target="test-en-0006-n" />
       <Example>"this is information"</Example>
@@ -105,11 +118,20 @@ Spanish:
       <SynsetRelation relType="hypernym" target="test-en-0004-n" />
     </Synset>
 
+    <Synset id="test-en-0005-n-fake" ili="" partOfSpeech="n" lexicalized="false" dc:subject="noun.cognition">
+      <Definition>a sample that is random</Definition>
+    </Synset>
+
     <Synset id="test-en-0006-n" ili="i67448" partOfSpeech="n" dc:subject="noun.cognition">
       <Definition>a measured or recorded piece of information</Definition>
       <SynsetRelation relType="hypernym" target="test-en-0001-n" />
     </Synset>
 
+    <Synset id="test-en-0007-v" ili="in" partOfSpeech="v" dc:subject="verb.social">
+      <!-- should probably be a hyponym of i33760 -->
+      <ILIDefinition>to fire someone while making it look like it was their idea</ILIDefinition>
+    </Synset>
+
   </Lexicon>
 
   <Lexicon id="test-es"
@@ -140,7 +162,7 @@ Spanish:
     <LexicalEntry id="test-es-ejemplificar-v">
       <Lemma partOfSpeech="v" writtenForm="ejemplificar" />
       <Sense id="test-es-ejemplificar-v-0003-01" synset="test-es-0003-v" >
-        <SenseRelation relType="derivation" target="test-es-ejemplo-v-0002-01" />
+        <SenseRelation relType="derivation" target="test-es-ejemplo-n-0002-01" />
       </Sense>
     </LexicalEntry>
 
@@ -154,7 +176,7 @@ Spanish:
     <LexicalEntry id="test-es-ilustrar-v">
       <Lemma partOfSpeech="v" writtenForm="ilustrar" />
       <Sense id="test-es-ilustrar-v-0004-01" synset="test-es-0003-v" >
-        <SenseRelation relType="derivation" target="test-es-ilustración-v-0002-01" />
+        <SenseRelation relType="derivation" target="test-es-ilustración-n-0002-01" />
       </Sense>
     </LexicalEntry>
 
diff --git a/tests/data/mini-lmf-1.1.xml b/tests/data/mini-lmf-1.1.xml
new file mode 100644
index 0000000..c95f767
--- /dev/null
+++ b/tests/data/mini-lmf-1.1.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE LexicalResource SYSTEM "http://globalwordnet.github.io/schemas/WN-LMF-1.1.dtd">
+
+<LexicalResource xmlns:dc="http://globalwordnet.github.io/schemas/dc/">
+
+  <Lexicon id="test-ja"
+           label="Testing Japanese WordNet"
+           language="ja"
+           email="maintainer@example.com"
+           license="https://creativecommons.org/licenses/by/4.0/"
+           version="1"
+           url="https://example.com/test-ja"
+           logo="logo.svg">
+
+    <Requires id="test-en" version="1" />
+
+    <LexicalEntry id="test-ja-情報-n">
+      <Lemma partOfSpeech="n" writtenForm="情報" script="Jpan" />
+      <Form id="test-ja-情報-n-じょうほう" writtenForm="じょうほう" script="Hira" />
+      <Form id="test-ja-情報-n-ジョウホウ" writtenForm="ジョウホウ" script="Kana" />
+      <Form id="test-ja-情報-n-zyouhou" writtenForm="zyouhou" script="Latn-kunrei" />
+      <Sense id="test-ja-情報-n-0001-01" synset="test-ja-0001-n" />
+    </LexicalEntry>
+
+    <LexicalEntry id="test-ja-例え-n">
+      <Lemma partOfSpeech="n" writtenForm="例え">
+        <Pronunciation variety="standard" notation="ipa" audio="tatoe.wav">tatoe</Pronunciation>
+      </Lemma>
+      <Form id="test-ja-例え-n-たとえ" writtenForm="たとえ" script="Hira" />
+      <Form id="test-ja-例え-n-タトエ" writtenForm="タトエ" script="Kana" />
+      <Form id="test-ja-例え-n-tatoe" writtenForm="tatoe" script="Latn-kunrei" />
+      <Sense id="test-ja-例え-n-0002-01" synset="test-ja-0002-n" />
+    </LexicalEntry>
+
+    <LexicalEntry id="test-ja-事例-n">
+      <Lemma partOfSpeech="n" writtenForm="事例" />
+      <Form id="test-ja-事例-n-じれい" writtenForm="じれい" script="Hira" />
+      <Form id="test-ja-事例-n-ジレイ" writtenForm="ジレイ" script="Kana" />
+      <Form id="test-ja-事例-n-zirei" writtenForm="zirei" script="Latn-kunrei" />
+      <Sense id="test-ja-事例-n-0002-01" synset="test-ja-0002-n" />
+    </LexicalEntry>
+
+    <Synset id="test-ja-0001-n" ili="i67447" partOfSpeech="n" lexfile="noun.cognition"
+            members="test-ja-情報-n-0001-01" />
+
+    <Synset id="test-ja-0002-n" ili="i67469" partOfSpeech="n" lexfile="noun.cognition"
+            members="test-ja-事例-n-0002-01 test-ja-例え-n-0002-01" />
+
+  </Lexicon>
+
+  <LexiconExtension id="test-en-ext"
+                    label="Testing English Extension"
+                    language="en"
+                    email="maintainer@example.com"
+                    license="https://creativecommons.org/licenses/by/4.0/"
+                    version="1"
+                    url="https://example.com/test-en-ext">
+
+    <Extends id="test-en" version="1" />
+
+    <!-- add sense relation -->
+    <ExternalLexicalEntry id="test-en-information-n">
+      <ExternalSense id="test-en-information-n-0001-01">
+        <SenseRelation relType="pertainym" target="test-en-ext-info-n-0001-01" />
+      </ExternalSense>
+    </ExternalLexicalEntry>
+
+    <!-- add a new entry for an existing synset -->
+    <LexicalEntry id="test-en-ext-info-n">
+      <Lemma partOfSpeech="n" writtenForm="info" />
+      <Sense id="test-en-ext-info-n-0001-01" synset="test-en-0001-n">
+        <SenseRelation relType="pertainym" target="test-en-information-n-0001-01" />
+      </Sense>
+    </LexicalEntry>
+
+    <!-- add a sense to an existing entry -->
+    <ExternalLexicalEntry id="test-en-illustrate-v">
+      <Sense id="test-en-ext-illustrate-v-0008-01" synset="test-en-ext-0008-v">
+	    <Example>"the artist illustrated the story beautifully"</Example>
+      </Sense>
+    </ExternalLexicalEntry>
+
+    <!-- add a tag -->
+    <ExternalLexicalEntry id="test-en-exemplify-v">
+      <ExternalLemma>
+        <Tag category="tense">INF</Tag>
+      </ExternalLemma>
+    </ExternalLexicalEntry>
+
+    <!-- add a new entry with a new synset -->
+    <LexicalEntry id="test-en-ext-fire-v">
+      <Lemma partOfSpeech="v" writtenForm="fire" />
+      <Sense id="test-en-ext-fire-v-0009-01" synset="test-en-ext-0009-v" subcat="social-transitive" />
+    </LexicalEntry>
+
+    <!-- only needed for ids -->
+    <ExternalSynset id="test-en-0001-n" />
+
+    <!-- add a relation to an existing synset -->
+    <ExternalSynset id="test-en-0007-v">
+      <SynsetRelation relType="hypernym" target="test-en-ext-0009-v" />
+    </ExternalSynset>
+
+    <Synset id="test-en-ext-0008-v" ili="i30181" partOfSpeech="v"
+            members="test-en-ext-illustrate-v-0008-01" lexfile="verb.creation">
+      <Definition>depict something in a visual medium</Definition>
+    </Synset>
+
+    <Synset id="test-en-ext-0009-v" ili="i33760" partOfSpeech="v"
+            members="test-en-ext-fire-v-0009-01" lexfile="verb.social">
+      <Definition>terminate employment</Definition>
+      <SynsetRelation relType="hyponym" target="test-en-0007-v" />
+    </Synset>
+
+    <SyntacticBehaviour id="social-transitive" subcategorizationFrame="Somebody ----s somebody" />
+
+  </LexiconExtension>
+
+</LexicalResource>
diff --git a/tests/db_test.py b/tests/db_test.py
index d531e6a..aaa8ff1 100644
--- a/tests/db_test.py
+++ b/tests/db_test.py
@@ -1,6 +1,7 @@
 
 import sqlite3
 import threading
+import tempfile
 
 import pytest
 
@@ -43,3 +44,24 @@ def set_w(self):
     WNThread()  # no error
     wn.config.allow_multithreading = False
     wn._db.pool = {}
+
+
+def test_remove_extension(mini_lmf_1_0, mini_lmf_1_1):
+    with tempfile.TemporaryDirectory('wn_data_1_1_trigger') as dir:
+        old_data_dir = wn.config.data_directory
+        wn.config.data_directory = dir
+        wn.add(mini_lmf_1_0)
+        wn.add(mini_lmf_1_1)
+        assert len(wn.lexicons()) == 4
+        wn.remove('test-en-ext')
+        assert len(wn.lexicons()) == 3
+        wn.remove('test-ja')
+        assert len(wn.lexicons()) == 2
+        wn.add(mini_lmf_1_1)
+        assert len(wn.lexicons()) == 4
+        wn.remove('test-en')
+        assert {lex.id for lex in wn.lexicons()} == {'test-es', 'test-ja'}
+        wn.config.data_directory = old_data_dir
+        # close any open DB connections before teardown
+        for conn in wn._db.pool.values():
+            conn.close()
diff --git a/tests/export_test.py b/tests/export_test.py
new file mode 100644
index 0000000..37e6abf
--- /dev/null
+++ b/tests/export_test.py
@@ -0,0 +1,24 @@
+
+from xml.etree import ElementTree as ET
+
+import pytest
+
+import wn
+
+
+@pytest.mark.usefixtures('mini_db')
+def test_export(mini_lmf_1_0, tmp_path):
+    tmpdir = tmp_path / 'test_export'
+    tmpdir.mkdir()
+    tmppath = tmpdir / 'mini_lmf_export.xml'
+    lexicons = wn.lexicons(lexicon='test-en test-es')
+    wn.export(lexicons, tmppath)
+
+    if hasattr(ET, 'canonicalize'):  # available from Python 3.8
+        # remove comments, indentation, etc.
+        orig = ET.canonicalize(from_file=mini_lmf_1_0, strip_text=True)
+        temp = ET.canonicalize(from_file=tmppath, strip_text=True)
+        # additional transformation to help with debugging
+        orig = orig.replace('<', '\n<')
+        temp = temp.replace('<', '\n<')
+        assert orig == temp
diff --git a/tests/lmf_test.py b/tests/lmf_test.py
index 6e6c082..b6c1205 100644
--- a/tests/lmf_test.py
+++ b/tests/lmf_test.py
@@ -1,15 +1,7 @@
 
-from wn import lmf
-
-
-class TestLexicon:
+from xml.etree import ElementTree as ET
 
-    def test_entry_ids(self):
-        lex = lmf.Lexicon(
-            'id', 'Test', 'tst', 'test@example.com', 'CC-BY 4.0', '1',
-            lexical_entries=[],
-            synsets=[])
-        assert lex.entry_ids() == set()
+from wn import lmf
 
 
 def test_is_lmf(datadir):
@@ -17,9 +9,10 @@ def test_is_lmf(datadir):
     assert lmf.is_lmf(str(datadir / 'mini-lmf-1.0.xml'))
     assert not lmf.is_lmf(datadir / 'README.md')
     assert not lmf.is_lmf(datadir / 'missing.xml')
+    assert lmf.is_lmf(datadir / 'mini-lmf-1.1.xml')
 
 
-def test_load(mini_lmf_1_0):
+def test_load_1_0(mini_lmf_1_0):
     lexicons = lmf.load(mini_lmf_1_0)
     assert len(lexicons) == 2
     lexicon = lexicons[0]
@@ -32,14 +25,14 @@ def test_load(mini_lmf_1_0):
     assert lexicon.version == '1'
     assert lexicon.url == 'https://example.com/test-en'
 
-    assert len(lexicon.lexical_entries) == 8
+    assert len(lexicon.lexical_entries) == 9
     le = lexicon.lexical_entries[0]
     assert le.id == 'test-en-information-n'
 
     assert le.lemma.form == 'information'
     assert le.lemma.pos == 'n'
-    assert le.lemma.script is None
-    assert len(le.lemma.tags) == 0
+    assert le.lemma.script == 'Latn'
+    assert len(le.lemma.tags) == 1
 
     assert len(le.forms) == 0
 
@@ -50,13 +43,45 @@ def test_load(mini_lmf_1_0):
     # assert le.senses[0].relations[0].target == 'test-en-exemplify-v-01023137-01'
     # assert le.senses[0].relations[0].type == 'derivation'
 
-    assert len(lexicon.synsets) == 6
+    assert len(lexicon.syntactic_behaviours) == 2
+    assert lexicon.syntactic_behaviours[0].frame == 'Somebody ----s something'
+    assert lexicon.syntactic_behaviours[0].senses == ['test-en-illustrate-v-0004-01']
+
+    assert len(lexicon.synsets) == 8
 
     assert lexicons[1].id == 'test-es'
 
 
-def test_dump(mini_lmf_1_0, tmp_path):
-    lexicons = lmf.load(mini_lmf_1_0)
+def test_load_1_1(mini_lmf_1_1):
+    lexicons = lmf.load(mini_lmf_1_1)
+    assert len(lexicons) == 2
+    lexicon = lexicons[0]
+    assert lexicon.id == 'test-ja'
+    assert lexicon.version == '1'
+    # assert lexicon.logo == 'logo.svg'
+    assert lexicon.requires == [{'id': 'test-en', 'version': '1', 'url': None}]
+
+    lexicon = lexicons[1]
+    assert lexicon.id == 'test-en-ext'
+    assert lexicon.extends == {'id': 'test-en', 'version': '1', 'url': None}
+
+
+def test_dump(mini_lmf_1_0, mini_lmf_1_1, tmp_path):
     tmpdir = tmp_path / 'test_dump'
     tmpdir.mkdir()
-    lmf.dump(lexicons, tmpdir / 'mini_lmf_dump.xml')
+    tmppath = tmpdir / 'mini_lmf_dump.xml'
+
+    def assert_xml_equal(mini_lmf, dump_lmf):
+        if hasattr(ET, 'canonicalize'):  # available from Python 3.8
+            orig = ET.canonicalize(from_file=mini_lmf, strip_text=True)
+            temp = ET.canonicalize(from_file=dump_lmf, strip_text=True)
+            # additional transformation to help with debugging
+            orig = orig.replace('<', '\n<')
+            temp = temp.replace('<', '\n<')
+            assert orig == temp
+
+    lmf.dump(lmf.load(mini_lmf_1_0), tmppath, version='1.0')
+    assert_xml_equal(mini_lmf_1_0, tmppath)
+
+    lmf.dump(lmf.load(mini_lmf_1_1), tmppath, version='1.1')
+    assert_xml_equal(mini_lmf_1_1, tmppath)
diff --git a/tests/primary_query_test.py b/tests/primary_query_test.py
index 9b658b1..58f4021 100644
--- a/tests/primary_query_test.py
+++ b/tests/primary_query_test.py
@@ -21,6 +21,8 @@ def test_lexicons_mini():
 
     results = wn.lexicons(lexicon='*')
     assert len(results) == 2
+    results = wn.lexicons(lexicon='*:1')
+    assert len(results) == 2
     results = wn.lexicons(lexicon='test-en')
     assert len(results) == 1 and results[0].language == 'en'
     results = wn.lexicons(lexicon='test-en:1')
@@ -28,6 +30,12 @@ def test_lexicons_mini():
     results = wn.lexicons(lexicon='test-en:*')
     assert len(results) == 1 and results[0].language == 'en'
 
+    assert wn.lexicons(lexicon='test-en')[0].specifier() == 'test-en:1'
+    assert wn.lexicons(lexicon='test-es')[0].specifier() == 'test-es:1'
+
+    assert wn.lexicons(lexicon='test-en')[0].requires() == {}
+    assert wn.lexicons(lexicon='test-es')[0].requires() == {}
+
 
 @pytest.mark.usefixtures('mini_db')
 def test_lexicons_unknown():
@@ -44,30 +52,33 @@ def test_words_empty():
 
 @pytest.mark.usefixtures('mini_db')
 def test_words_mini():
-    assert len(wn.words()) == 14
+    assert len(wn.words()) == 15
     assert all(isinstance(w, wn.Word) for w in wn.words())
 
     words = wn.words('information')  # search lemma
     assert len(words) == 1
     assert words[0].lemma() == 'information'
 
+    assert words[0].lemma().script == 'Latn'
+    assert words[0].lemma().tags() == [wn.Tag('tag-text', 'tag-category')]
+
     words = wn.words('exemplifies')  # search secondary form
     assert len(words) == 1
     assert words[0].lemma() == 'exemplify'
 
     assert len(wn.words(pos='n')) == 10
     assert all(w.pos == 'n' for w in wn.words(pos='n'))
-    assert len(wn.words(pos='v')) == 4
+    assert len(wn.words(pos='v')) == 5
     assert len(wn.words(pos='q')) == 0  # fake pos
 
-    assert len(wn.words(lang='en')) == 8
+    assert len(wn.words(lang='en')) == 9
     assert len(wn.words(lang='es')) == 6
 
-    assert len(wn.words(lexicon='test-en')) == 8
+    assert len(wn.words(lexicon='test-en')) == 9
     assert len(wn.words(lexicon='test-es')) == 6
 
-    assert len(wn.words(lang='en', lexicon='test-en')) == 8
-    assert len(wn.words(pos='v', lang='en')) == 2
+    assert len(wn.words(lang='en', lexicon='test-en')) == 9
+    assert len(wn.words(pos='v', lang='en')) == 3
     assert len(wn.words('information', lang='en')) == 1
     assert len(wn.words('information', lang='es')) == 0
 
@@ -105,29 +116,30 @@ def test_senses_empty():
 
 @pytest.mark.usefixtures('mini_db')
 def test_senses_mini():
-    assert len(wn.senses()) == 14
+    assert len(wn.senses()) == 16
     assert all(isinstance(s, wn.Sense) for s in wn.senses())
 
     senses = wn.senses('information')  # search lemma
     assert len(senses) == 1
     assert senses[0].word().lemma() == 'information'
+    assert senses[0].counts() == [3]
 
     senses = wn.senses('exemplifies')  # search secondary form
     assert len(senses) == 1
     assert senses[0].word().lemma() == 'exemplify'
 
-    assert len(wn.senses(pos='n')) == 10
-    assert len(wn.senses(pos='v')) == 4
+    assert len(wn.senses(pos='n')) == 11
+    assert len(wn.senses(pos='v')) == 5
     assert len(wn.senses(pos='q')) == 0  # fake pos
 
-    assert len(wn.senses(lang='en')) == 8
+    assert len(wn.senses(lang='en')) == 10
     assert len(wn.senses(lang='es')) == 6
 
-    assert len(wn.senses(lexicon='test-en')) == 8
+    assert len(wn.senses(lexicon='test-en')) == 10
     assert len(wn.senses(lexicon='test-es')) == 6
 
-    assert len(wn.senses(lang='en', lexicon='test-en')) == 8
-    assert len(wn.senses(pos='v', lang='en')) == 2
+    assert len(wn.senses(lang='en', lexicon='test-en')) == 10
+    assert len(wn.senses(pos='v', lang='en')) == 3
     assert len(wn.senses('information', lang='en')) == 1
     assert len(wn.senses('information', lang='es')) == 0
 
@@ -165,7 +177,7 @@ def test_synsets_empty():
 
 @pytest.mark.usefixtures('mini_db')
 def test_synsets_mini():
-    assert len(wn.synsets()) == 10
+    assert len(wn.synsets()) == 12
     assert all(isinstance(ss, wn.Synset) for ss in wn.synsets())
 
     synsets = wn.synsets('information')  # search lemma
@@ -176,21 +188,21 @@ def test_synsets_mini():
     assert len(synsets) == 1
     assert 'exemplify' in synsets[0].lemmas()
 
-    assert len(wn.synsets(pos='n')) == 8
-    assert len(wn.synsets(pos='v')) == 2
+    assert len(wn.synsets(pos='n')) == 9
+    assert len(wn.synsets(pos='v')) == 3
     assert len(wn.synsets(pos='q')) == 0  # fake pos
 
     assert len(wn.synsets(ili='i67469')) == 2
     assert len(wn.synsets(ili='i67468')) == 0
 
-    assert len(wn.synsets(lang='en')) == 6
+    assert len(wn.synsets(lang='en')) == 8
     assert len(wn.synsets(lang='es')) == 4
 
-    assert len(wn.synsets(lexicon='test-en')) == 6
+    assert len(wn.synsets(lexicon='test-en')) == 8
     assert len(wn.synsets(lexicon='test-es')) == 4
 
-    assert len(wn.synsets(lang='en', lexicon='test-en')) == 6
-    assert len(wn.synsets(pos='v', lang='en')) == 1
+    assert len(wn.synsets(lang='en', lexicon='test-en')) == 8
+    assert len(wn.synsets(pos='v', lang='en')) == 2
     assert len(wn.synsets('information', lang='en')) == 1
     assert len(wn.synsets('information', lang='es')) == 0
     assert len(wn.synsets(ili='i67469', lang='es')) == 1
@@ -220,3 +232,40 @@ def test_synset_mini():
         assert wn.synset('test-es-0001-n', lang='unk')
     with pytest.raises(wn.Error):
         assert wn.synset('test-es-0001-n', lexicon='test-unk')
+
+
+@pytest.mark.usefixtures('mini_db_1_1')
+def test_mini_1_1():
+    assert len(wn.lexicons()) == 4
+    assert len(wn.lexicons(lang='en')) == 2
+    assert len(wn.lexicons(lang='ja')) == 1
+    assert wn.lexicons(lang='ja')[0].logo == 'logo.svg'
+
+    w = wn.Wordnet(lang='en')
+    assert len(w.lexicons()) == 2
+    assert len(w.expanded_lexicons()) == 0
+    assert len(w.word('test-en-exemplify-v').lemma().tags()) == 1
+
+    w = wn.Wordnet(lang='ja')
+    assert len(w.lexicons()) == 1
+    assert len(w.expanded_lexicons()) == 1
+    assert len(w.synsets('例え')[0].hypernyms()) == 1
+    assert w.synsets('例え')[0].lexfile() == 'noun.cognition'
+    assert len(w.word('test-ja-例え-n').lemma().pronunciations()) == 1
+    assert w.word('test-ja-例え-n').forms()[1].id == 'test-ja-例え-n-たとえ'
+    p = w.word('test-ja-例え-n').lemma().pronunciations()[0]
+    assert p.value == 'tatoe'
+    assert p.variety == 'standard'
+    assert p.notation == 'ipa'
+    assert p.phonemic
+    assert p.audio == 'tatoe.wav'
+
+    w = wn.Wordnet(lang='ja', expand='')
+    assert len(w.lexicons()) == 1
+    assert len(w.expanded_lexicons()) == 0
+    assert len(w.synsets('例え')[0].hypernyms()) == 0
+
+    w = wn.Wordnet(lexicon='test-en test-en-ext')
+    assert len(w.lexicons()) == 2
+    assert len(w.expanded_lexicons()) == 0
+    assert len(w.synsets('fire')[0].hyponyms()) == 1
diff --git a/tests/relations_test.py b/tests/relations_test.py
index 9ebe321..7d71251 100644
--- a/tests/relations_test.py
+++ b/tests/relations_test.py
@@ -36,5 +36,47 @@ def test_synset_hypernyms_expand_empty():
 def test_synset_hypernyms_expand_specified():
     w = wn.Wordnet(lang='es', expand='test-en')
     assert w.synset('test-es-0002-n').hypernyms() == [
-        wn.synset('test-es-0001-n')
+        w.synset('test-es-0001-n')
+    ]
+
+
+@pytest.mark.usefixtures('mini_db_1_1')
+def test_extension_relations():
+    # default mode
+    assert wn.synset('test-en-0007-v').hypernyms() == [
+        wn.synset('test-en-ext-0009-v')
+    ]
+    assert wn.synset('test-en-ext-0009-v').hyponyms() == [
+        wn.synset('test-en-0007-v')
+    ]
+    assert wn.sense('test-en-information-n-0001-01').get_related('pertainym') == [
+        wn.sense('test-en-ext-info-n-0001-01')
+    ]
+    assert wn.sense('test-en-ext-info-n-0001-01').get_related('pertainym') == [
+        wn.sense('test-en-information-n-0001-01')
     ]
+
+    # restricted to base
+    w = wn.Wordnet(lexicon='test-en')
+    assert w.synset('test-en-0007-v').hypernyms() == []
+    assert w.sense('test-en-information-n-0001-01').get_related('pertainym') == []
+
+    # base and extension
+    w = wn.Wordnet(lexicon='test-en test-en-ext')
+    assert w.synset('test-en-0007-v').hypernyms() == [
+        w.synset('test-en-ext-0009-v')
+    ]
+    assert w.synset('test-en-ext-0009-v').hyponyms() == [
+        w.synset('test-en-0007-v')
+    ]
+    assert w.sense('test-en-information-n-0001-01').get_related('pertainym') == [
+        w.sense('test-en-ext-info-n-0001-01')
+    ]
+    assert w.sense('test-en-ext-info-n-0001-01').get_related('pertainym') == [
+        w.sense('test-en-information-n-0001-01')
+    ]
+
+    # restricted to extension
+    w = wn.Wordnet(lexicon='test-en-ext')
+    assert w.synset('test-en-ext-0009-v').hyponyms() == []
+    assert w.sense('test-en-ext-info-n-0001-01').get_related('pertainym') == []
diff --git a/tests/secondary_query_test.py b/tests/secondary_query_test.py
index 671e67f..39ae397 100644
--- a/tests/secondary_query_test.py
+++ b/tests/secondary_query_test.py
@@ -50,6 +50,15 @@ def test_sense_lexicalized():
     assert wn.sense('test-es-información-n-0001-01').lexicalized()
 
 
+@pytest.mark.usefixtures('mini_db')
+def test_sense_frames():
+    assert wn.sense('test-en-illustrate-v-0004-01').frames() == [
+        'Somebody ----s something',
+        'Something ----s something',
+    ]
+    assert wn.sense('test-es-ilustrar-v-0004-01').frames() == []
+
+
 @pytest.mark.usefixtures('mini_db')
 def test_sense_translate():
     assert len(wn.sense('test-en-information-n-0001-01').translate(lang='es')) == 1