Hmm, still not much progress :-( Although then again, I had taken the day off to help some friends move only to find I was only needed the morning.
Anyway, reading the manual on embedded GATE, two new points:
- you can use the clicky-clicky interface to create a pipeline, which you then save into a sort of XML file that you can then run using the Gate API
- their representation of annotations is a DAG (looks familiar if you've used the connecting nodes notation for chart parsers):

I wonder why it's important that it's directed, or acyclic for that matter. Seems like all you really care about in an annotation is that nodes (a) and (b) are connected by annotation 42. I guess if you're going to say it's directed, acyclic makes sense.
not GATE
I should clarify that I'm not particularly attached to using GATE per se; I'm just going to assume they know what they're doing and borrow as much from them as possible, including model and any intermediary formats for storing things. If this makes it easier for me to plug into GATE (or plug GATE into what I'm trying to do, so much the better).
thinking in circles
So what's potentially tricky for me is still to imagine how all this is going to fit together. Say I have a translation layer between the annotation-backend and the linguistic layer. As input, the translation layer is getting HTML formatted text. This tells me where all the formatting information, pre-existing annotations, and paragraphs are; but if I want to stick to a node-id model and not a character-id model, I'm going to have to introduce some sort of tokenisation process. But OK, then how do I line up the original HTML with the output tokens?
Let's focus on paragraphs now. How I say where all the paragraphs are exactly? I don't care about your fancy bolds and italics, but if I squish your paragraphs together, you're going to be one unhappy manual annotator. I know which strings they wrap around, so one simple way is just to ditch all the formatting, and all the pre-existing annotations, and tokenise each paragraph separately.
Alternatively, we could perhaps try to take them into account by making the simplifying assumption that any pre-existing annotation necessarily introduces a token boundary. In other words, if you see
hello, you've instantly got two tokens
hel and lo. What you would do in effect is to start out by doing a crude tokenisation step that breaks up the text by whitespace (treating any markup node as introducing whitespace on both ends). This gives you an initial annotation graph with a list of tokens and paragraphs spanning some of those tokens. After this, you
re-tokenise using your linguistic black magic... this means that some of your tokens may be merged or split, which has all sorts of potentially weird consequences for your paragraph. How would you know how to wibble your paragraph boundaries? Do you have to compare the token lists and somehow infer if they've been split or merged?
Now maybe the only thing to do is work on the character level at some early point, but still output token-based outputs. For that to work your tokeniser may have to tell you which characters it's spanning. That or you'd have to adopt some kind of whitespace folding scheme. Argh, thinking myself in circles.
How do you automatically annotate a text while preserving any pre-existing formatting and annotations on it? It sounds really simple: just use a stand-off annotation approach, but (having not a single clue what I'm doing), it seems that your choice of nodes (strings vs characters) could make things potentially tricky.